diff --git a/.env.example b/.env.example index 73b87e9d93..c741ba39c6 100644 --- a/.env.example +++ b/.env.example @@ -498,6 +498,18 @@ NEXT_PUBLIC_CLOUD_URL= #OPENCODE_GO_AUTH_COOKIE=auth=... #OMNIROUTE_OPENCODE_GO_AUTH_COOKIE=auth=... +# OpenCode Go/Zen VPS egress (#5997): on a datacenter VPS, Cloudflare in front of +# opencode.ai/zen/go 403s chat requests that lack OpenCode CLI identity headers. +# When your clients don't already send them, set this to synthesize the CLI headers +# (User-Agent, x-opencode-client, x-opencode-project, fresh request/session UUIDs) on +# absent keys. OFF by default — forward-only is safer when clients already send them. +# Values are overridable via OPENCODE_GO_USER_AGENT / OPENCODE_USER_AGENT / OPENCODE_CLIENT / +# OPENCODE_PROJECT (defaults: opencode-cli/1.0.0 / cli / default). +#OPENCODE_SYNTHESIZE_CLI_HEADERS=true +#OPENCODE_USER_AGENT=opencode-cli/1.0.0 +#OPENCODE_CLIENT=cli +#OPENCODE_PROJECT=default + # Ollama Cloud quota scraping. Prefer configuring this per connection in # Dashboard → Providers → Ollama Cloud. The cookie is sensitive. #OLLAMA_USAGE_COOKIE=__Secure-session=... @@ -1186,7 +1198,7 @@ CURSOR_USER_AGENT="Cursor/3.4" APP_LOG_TO_FILE=true # Path to the application log file. -# Default: logs/application/app.log (relative to project root / DATA_DIR) +# Default: /logs/application/app.log (DATA_DIR defaults to ~/.omniroute) # APP_LOG_FILE_PATH=logs/application/app.log # Maximum single log file size before rotation. @@ -1521,6 +1533,11 @@ APP_LOG_TO_FILE=true # PROXY_AUTO_REMOVE=false # Consecutive failures before an auto-remove fires. Default: 3. # PROXY_AUTO_REMOVE_AFTER=3 +# Let automated reachability probes (the scheduler + the "Test All" button) WRITE +# a proxy's status. Default "false": probes are read-only and never deactivate a +# proxy — only the operator sets active/inactive (a flaky probe must not strand an +# assigned proxy; #6246). Set "true" to restore the legacy test-and-set behaviour. +# PROXY_HEALTH_AUTO_DEACTIVATE=false # Allow OAuth and provider validation flows to bypass a pinned proxy and connect # directly when proxy reachability pre-checks fail. Default: false. @@ -1703,6 +1720,20 @@ APP_LOG_TO_FILE=true # Routing-decision log verbosity: 0 silences, higher values log more bypass/route # decisions (src/mitm/server.cjs, _internal/bypass.cjs). # MITM_VERBOSE=1 +# Strip the leading `sudo` from MITM cert-trust commands (src/mitm/systemCommands.ts) — +# for root-less / user-namespaced deployments (e.g. rootless Docker/Podman) +# where the operator trusts the CA manually (e.g. via Node's extra-CA-certs mechanism). +# OMNIROUTE_NO_SUDO=0 + +# ── Test/CI-only guards (never needed in production) ── +# Set automatically by tests/_setup/isolateDataDir.ts and the CI workflows: the +# test suite must NEVER mutate the OS trust store (a fake test PEM installed via +# update-ca-certificates broke all system TLS on a persistent runner, 2026-07-05). +# OMNIROUTE_SKIP_SYSTEM_TRUST=1 +# check-changelog-integrity.mjs (anti CHANGELOG-eat gate): explicit base ref +# override, and the justified-removal escape hatch for intentional bullet removals. +# CHANGELOG_BASE_REF=origin/release/v0.0.0 +# ALLOW_CHANGELOG_REMOVALS=1 # ── 1Proxy egress pool ── # Used by: src/lib/oneproxySync.ts — fetches proxy nodes from the OmniRoute diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 988f844aaf..706b6c22f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,10 @@ permissions: contents: read env: + # CI must never mutate the runner's OS trust store (2026-07-05: a cert-flow + # test installed a fake PEM on a persistent self-hosted runner and broke all + # system TLS). Belt-and-suspenders with tests/_setup/isolateDataDir.ts. + OMNIROUTE_SKIP_SYSTEM_TRUST: "1" CI_NODE_VERSION: "24" CI_NODE_24_VERSION: "24" CI_NODE_26_VERSION: "26" @@ -506,7 +510,15 @@ jobs: build: name: Build - runs-on: ubuntu-latest + # Dynamic runner: when the release captain flips the USE_VPS_RUNNER repo var to + # 'true' (scripts/vps/release-runner-up.sh does it after the self-hosted VM is + # online), the heavy jobs run on the dedicated 32-core VPS runners (label + # omni-release) instead of queueing on the 20-concurrent-job hosted pool. + # Safety: fork PRs NEVER reach the self-hosted runner — the expression falls + # back to ubuntu-latest unless the PR head repo is this repository (push / + # dispatch events are own-origin by definition). Any failure path (VM down, + # var unset/false) also falls back to ubuntu-latest. + runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }} needs: changes if: ${{ github.event_name != 'pull_request' || (needs.changes.outputs.code == 'true' && github.event.pull_request.draft == false) }} steps: @@ -519,14 +531,21 @@ jobs: cache: npm - uses: ./.github/actions/npm-ci-retry - run: npm run check:node-runtime - - name: Cache Next.js build cache - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - with: - path: .build/next/cache - key: nextjs-${{ runner.os }}-node-${{ env.CI_NODE_VERSION }}-${{ hashFiles('package-lock.json') }}-${{ hashFiles('src/**/*', 'open-sse/**/*', 'db/**/*', 'next.config.mjs', 'tsconfig*.json', 'postcss.config.*', 'tailwind.config.*') }} - restore-keys: | - nextjs-${{ runner.os }}-node-${{ env.CI_NODE_VERSION }}-${{ hashFiles('package-lock.json') }}- + # NOTE: the webpack `.build/next/cache` actions/cache step was removed with the + # Turbopack switch below — Turbopack does not read/write the webpack cache dir + # (its persistent FS cache is still experimental and intentionally NOT enabled), + # so restoring the old ~0.5 GB webpack cache would only waste download time. + # Rolling back to webpack = revert this commit (cache step comes back with it). + # + # Turbopack production build (Next 16, stable): benchmarked 1.9× faster than the + # webpack pass (9min0s vs 17min15s on a 32-core box; multi-core Rust vs webpack's + # single-threaded compile). Standalone output smoke-validated (server boots, + # /api/monitoring/health 200). Downstream jobs (e2e ×9, package-artifact, + # electron-package-smoke) consume this artifact, so a green run here validates + # the Turbopack artifact end-to-end. - run: npm run build + env: + OMNIROUTE_USE_TURBOPACK: "1" - name: Archive Next.js build for downstream jobs # Use tar so the archive preserves paths relative to CWD (.build/next/...). # upload-artifact path-stripping is ambiguous when exclude patterns are used; @@ -614,9 +633,16 @@ jobs: test-unit: name: Unit Tests (${{ matrix.shard }}/8) - runs-on: ubuntu-latest + # Same dynamic-runner rule as Build (own-origin only; fallback ubuntu-latest). + runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }} timeout-minutes: 25 - needs: build + # needs: changes (not build) — this job never downloads the next-build artifact; + # gating it on Build only serialized ~20min of wall-clock for nothing. Jobs that + # DO consume the artifact (e2e, package-artifact, electron-package-smoke) keep + # needs: build. The `if` mirrors Build's own skip condition so docs-only PRs + # still skip the suite. + needs: changes + if: ${{ github.event_name != 'pull_request' || (needs.changes.outputs.code == 'true' && github.event.pull_request.draft == false) }} strategy: fail-fast: false matrix: @@ -662,9 +688,12 @@ jobs: test-vitest: name: Vitest (MCP / autoCombo / UI components) - runs-on: ubuntu-latest + # Same dynamic-runner rule as Build (own-origin only; fallback ubuntu-latest). + runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }} timeout-minutes: 15 - needs: build + # needs: changes (not build) — no artifact consumed; see test-unit note. + needs: changes + if: ${{ github.event_name != 'pull_request' || (needs.changes.outputs.code == 'true' && github.event.pull_request.draft == false) }} env: JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation API_KEY_SECRET: ci-test-api-key-secret-long @@ -952,7 +981,9 @@ jobs: name: Integration Tests (${{ matrix.shard }}/2) runs-on: ubuntu-latest timeout-minutes: 15 - needs: build + # needs: changes (not build) — no artifact consumed; see test-unit note. + needs: changes + if: ${{ github.event_name != 'pull_request' || (needs.changes.outputs.code == 'true' && github.event.pull_request.draft == false) }} strategy: fail-fast: false matrix: @@ -979,7 +1010,9 @@ jobs: test-security: name: Security Tests runs-on: ubuntu-latest - needs: build + # needs: changes (not build) — no artifact consumed; see test-unit note. + needs: changes + if: ${{ github.event_name != 'pull_request' || (needs.changes.outputs.code == 'true' && github.event.pull_request.draft == false) }} env: JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation API_KEY_SECRET: ci-test-api-key-secret-long diff --git a/.github/workflows/nightly-release-green.yml b/.github/workflows/nightly-release-green.yml index c1d5128a74..81c599a7a9 100644 --- a/.github/workflows/nightly-release-green.yml +++ b/.github/workflows/nightly-release-green.yml @@ -31,10 +31,17 @@ concurrency: group: nightly-release-green cancel-in-progress: true +env: + OMNIROUTE_SKIP_SYSTEM_TRUST: "1" + jobs: release-green: name: Validate active release branch - runs-on: ubuntu-latest + # Dynamic runner: with USE_VPS_RUNNER=true (release window / on-demand pre-flight) + # this runs on the dedicated VPS runner — clean env (no operator OMNIROUTE_API_KEY, + # no local noauth CLIs => zero machine-specific false positives) and no contention. + # Nightly cron normally finds the var false (VM off) and falls back to hosted. + runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && fromJSON('["self-hosted","omni-release"]')) || 'ubuntu-latest' }} env: JWT_SECRET: ci-nightly-secret-with-sufficient-length-for-validation API_KEY_SECRET: ci-nightly-api-key-secret-long @@ -88,7 +95,9 @@ jobs: id: validate run: | set +e - node scripts/quality/validate-release-green.mjs --json --with-build \ + # --hermetic: scrub live-test trigger vars (self-hosted runner may carry + # operator env; hosted ignores the unknown flag before #6300 lands). + node scripts/quality/validate-release-green.mjs --json --with-build --hermetic \ 1> release-green.json 2> release-green.log echo "exit=$?" >> "$GITHUB_OUTPUT" echo "------- report -------" diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 2d13fe3dc4..f5c0aa9967 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -14,6 +14,10 @@ permissions: contents: read env: + # CI must never mutate the runner's OS trust store (2026-07-05: a cert-flow + # test installed a fake PEM on a persistent self-hosted runner and broke all + # system TLS). Belt-and-suspenders with tests/_setup/isolateDataDir.ts. + OMNIROUTE_SKIP_SYSTEM_TRUST: "1" CI_NODE_VERSION: "24" jobs: @@ -174,3 +178,37 @@ jobs: - run: npm ci - name: ESLint (baseline congelado — warning novo = vermelho) run: npx eslint . --suppressions-location config/quality/eslint-suppressions.json --max-warnings 0 + + # Merge-integrity: pega no PR os dois vazamentos crônicos de merge que hoje só + # explodem na release-PR. (1) CHANGELOG-eat — o auto-resolve do merge come + # bullets vizinhos/seções inteiras (incidente #6193, 2026-07-05: 212 linhas / + # 130 bullets); o checkout de PR é refs/pull/N/merge, então comparar contra a + # base detecta o eat ANTES do merge. (2) SKILL.md gerado stale vs o catálogo de + # agent-skills (#6186 mergeou um id de catálogo sem rodar o gerador → 8 reds de + # integration invisíveis até a release). + # + # Princípio Zero: bloqueante SÓ para branches internas; PR de FORK roda em modo + # report (continue-on-error) — a campanha corrige via co-autoria, o contribuidor + # nunca é bloqueado. + merge-integrity: + name: Merge integrity (changelog + generated skills) + runs-on: ubuntu-latest + continue-on-error: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }} + env: + JWT_SECRET: ci-lint-secret-with-sufficient-length-for-validation + API_KEY_SECRET: ci-lint-api-key-secret-long + DISABLE_SQLITE_AUTO_BACKUP: "true" + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + fetch-depth: 0 + persist-credentials: false + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + with: + node-version: ${{ env.CI_NODE_VERSION }} + cache: npm + - run: npm ci + - name: CHANGELOG integrity (nenhum bullet da base pode sumir no merge-result) + run: npm run check:changelog-integrity + - name: Agent-skills generator sync (SKILL.md gerado ≡ catálogo) + run: npm run check:agent-skills-sync diff --git a/.gitignore b/.gitignore index bb05ae4c44..b2261dfff7 100644 --- a/.gitignore +++ b/.gitignore @@ -232,3 +232,4 @@ omniroute.md # mise configuration mise.toml +_artifacts/ diff --git a/CHANGELOG.md b/CHANGELOG.md index e7e6201aa7..2c4378425c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,471 @@ ## [Unreleased] +--- + +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(proxy):** native **proxy-pool round-robin / egress IP rotation** ([#6365](https://github.com/diegosouzapw/OmniRoute/issues/6365)) — a scope (global / provider / account) can now hold **multiple** proxies as a pool with a rotation strategy, so outbound requests cycle their egress IP instead of pinning one proxy per scope. Migration `117_proxy_pool_rotation.sql` lifts the `UNIQUE(scope, scope_id)` constraint (rebuild via the canonical rename/copy/drop; existing single assignments become 1-element pools) and adds a `proxy_scope_rotation` companion table holding the per-scope strategy + a persisted monotonic round-robin cursor. Strategies: `round-robin` (default, monotonic cursor — never `Math.random`), `random`, and `sticky-per-N-min`. Resolution (`resolveProxyForScopeFromRegistry` / `resolveProxyForConnectionFromRegistry`) now fetches the alive, position-ordered candidate set (unchanged `PROXY_ALIVE_PREDICATE`) and applies the strategy; an empty / all-dead pool still returns `null` — the #6246 fail-closed guard is **untouched** (never falls through to direct egress). Backend + DB only; dashboard pool-builder UI is a follow-up. Regression guard: `tests/unit/proxy-pool-rotation-6365.test.ts` (8, incl. fail-closed + backward-compat). +- **feat(providers):** end-to-end **tool/function calling on the native Gemini `/v1beta` endpoint** ([#6222](https://github.com/diegosouzapw/OmniRoute/issues/6222)) — both directions of the Gemini↔OpenAI conversion now preserve tool data (previously silently dropped). Request side: `convertGeminiToInternal` (extracted to its own testable module) maps `tools[].functionDeclarations` → OpenAI `tools`, prior `functionCall` parts → assistant `tool_calls`, and `functionResponse` parts → `tool`-role messages. Response side: `convertOpenAIResponseToGemini` emits `parts[].functionCall {name,args}` from `message.tool_calls`, and the streaming `openAIChunkToGeminiChunk` accumulates fragmented `tool_calls` deltas by index into complete `functionCall` parts. The non-Gemini client paths (Claude, OpenAI-Responses) already preserved tool calls — this closes the gap specific to the native Gemini surface. Regression guard: `tests/unit/v1beta-gemini-tool-calling-6222.test.ts` (6, incl. a streaming SSE round-trip). +- **feat(providers):** copilot-m365-web **enterprise / work tier** support ([#6334](https://github.com/diegosouzapw/OmniRoute/issues/6334)) — mirrors the EDU-tier pattern (#6210): `M365ConnectionParams` gains an `agent` field, a new opt-in `M365_ENTERPRISE_OVERRIDES` preset (`agent=work`, `scenario=officeweb`, `licenseType=Premium`) applies via `providerSpecificData.tier="enterprise"` (alias `"work"`), and `agent` is also overridable directly via `providerSpecificData.agent`. `buildWsUrl` was hardcoding `agent="web"` (the one enterprise-distinguishing param with no override path), so a Premium work account handshook then returned an empty stream. The individual and EDU paths are untouched. Kilo's dup flag vs #6210 (EDU tier) was a false positive — different tier. Regression guard: `tests/unit/copilot-m365-enterprise-6334.test.ts` (7). End-to-end confirmation on a real Premium work account is a live-VPS validation follow-up (Hard Rule #18). (thanks @Forcerecon) +- **feat(api):** standardized, provider-agnostic **`effort` + `thinking` request params** ([#6241](https://github.com/diegosouzapw/OmniRoute/issues/6241)) — a thin standardization layer over the existing mature per-provider reasoning plumbing (no provider mapper touched). `providerChatCompletionSchema` gains a canonical `effort` (reusing the shared `none/low/medium/high/xhigh` vocabulary — the UI tiers `extra`/`max` collapse onto `xhigh`) and a boolean `thinking`. A pure `normalizeReasoningRequest` (wired once in `src/sse/handlers/chat.ts`, before any reasoning field is read) folds them onto the fields the translators already consume (`reasoning_effort` / `reasoning.effort` / `thinking`), so they fan out to Anthropic / Gemini / xAI / Responses — an explicit client `reasoning_effort` / object-shaped `thinking` always wins (backward-compatible). `/models` additively exposes `supportsThinking` + `effort_tiers` so the frontend can render the toggles (UI component is a follow-up). Regression guard: `tests/unit/effort-thinking-standardization-6241.test.ts` (12). (thanks @Iammilansoni, @shabeer) +- **feat(combo):** new **`pipeline` (sequential) combo strategy** ([#6297](https://github.com/diegosouzapw/OmniRoute/issues/6297)) — the 18th routing strategy runs targets **in order**, threading each step's output into the next step's input, with an optional per-step `prompt` (system instruction); only the final step's response is returned. Distinct from `fusion` (parallel fan-out + judge). Implemented as a self-contained `open-sse/services/pipeline.ts` (sibling to `fusion.ts`), dispatched from `combo.ts`; the step list reuses `combo.models` order and reads an optional `prompt` off each target (backward-compatible — ignored by every other strategy). Intermediate steps run non-streaming with tools stripped (complete prose to thread forward); the final step keeps the client's `stream` flag + tools. A failing/empty/unparseable intermediate step fails the whole pipeline explicitly via a sanitized error (never silently swallowed). Kilo's dup flag vs #563 was a false positive (that's model→chain selection; this is a sequential chain). Regression guard: `tests/unit/combo-pipeline-strategy.test.ts` (5). (thanks @ofekbetzalel) +- **feat(ci):** `check:test-masking` now flags **inline-reimplemented prod conditions** ([#6348](https://github.com/diegosouzapw/OmniRoute/issues/6348)) — a new **report-only** subcheck (v2, 6A.10 family) catches the wrong-shape contract test: a test that recomputes the condition under test inline instead of importing/exercising the real function (the #6216 class, where `=== 500` → `>= 500` stayed green because the test re-implemented the branch). For each added/modified test file it warns when the file textually duplicates a ≥3-token conditional from a production file touched in the same PR **and** does not import the symbol/module owning it, via a pure, fixture-tested `findReimplementedConditions()` with an allowlist mirroring `assertReductionAllowlist`. Report-only for now (does not fail the gate) — to be promoted to blocking after a triage cycle. Regression guard: `tests/unit/check-test-masking.test.ts` (45). +- **feat(sse):** per-connection routing override (native vs CLIProxyAPI) ([#6339](https://github.com/diegosouzapw/OmniRoute/issues/6339)) — the previously-dead `isCliproxyapiDeepModeEnabled` helper is now wired into `resolveExecutorWithProxy`: a single connection can opt itself into the CLIProxyAPI passthrough executor via `providerSpecificData.cliproxyapiMode="claude-native"`, with precedence **connection override > provider `upstream_proxy_config` mode > default**. `resolveExecutorWithProxy` now receives the resolved connection's `providerSpecificData` (threaded from `chatCore.ts`), so one connection can deep-route while the provider's other connections stay native — no DB schema change (the toggle rides in `providerSpecificData`). Also resolves the same-provider mixing ask in #6340. Regression guard: `tests/unit/chatcore-executor-proxy.test.ts` (9). (thanks @RaviTharuma) +- **feat(dashboard):** "Add session cookie" modal now shows a prominent **"Open ‹host› →"** link to the provider's own site ([#6268](https://github.com/diegosouzapw/OmniRoute/issues/6268)) — every `-web` cookie-session provider (chatgpt-web, claude-web, gemini-web, kimi-web, lmarena, qwen-web, m365-copilot-web, …) renders a one-click external link (opening the provider's login/home page in a new tab) so operators no longer tab away to retype the URL mid-setup. The host resolves from a pure, unit-tested `resolveWebProviderHost()` (prefers `WEB_COOKIE_PROVIDERS[id].website`, falls back to the registry `baseUrl` origin); non-web providers render exactly as before. Kilo's dup flag vs #6265 (modal-too-small-on-1080p) was a false positive — distinct concern. Regression guard: `tests/unit/resolve-web-provider-host.test.ts` (5). (thanks @chirag127) +- **feat(providers):** add **DigitalOcean** AI (serverless inference) as an OpenAI-compatible API-key provider — base `https://inference.do-ai.run/v1`, wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/digitalocean/`, `src/shared/constants/providers/apikey/inference-hosts.ts`). Regression guard: `tests/unit/digitalocean-provider.test.ts`. (thanks @newnol) +- **feat(providers):** add **Huancheng Public API** (`hcnsec`) as an OpenAI-compatible regional provider — Xinjiang Huancheng Cybersecurity's public LLM platform (base `https://api.hcnsec.cn/v1`, free credits via daily check-ins), wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/hcnsec/`, `src/shared/constants/providers/apikey/regional.ts`). Regression guard: `tests/unit/hcnsec-provider.test.ts`. (thanks @UnrealAryan) +- **feat(dashboard):** the web-session credential guide now shows an **"Open {host}" link** to the provider's sign-in site (derived from the provider `website` via `getProviderWebsiteHost`), so you can jump straight to the page where the cookie/session must be captured. Regression guard: `tests/unit/web-session-provider-link-6316.test.ts`. (thanks @jordansilly77-stack) +- **feat(cerebras):** add the **Gemma 4 31B** model (`gemma-4-31b`) to the Cerebras registry + pricing table. Regression guard extends `tests/unit/t28-model-catalog-updates.test.ts`. (thanks @backryun) +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + ### 🐛 Bug Fixes +- **feat(providers):** add **Zed** hosted LLM aggregator as a native-app provider ([#6118](https://github.com/diegosouzapw/OmniRoute/pull/6118)) — OAuth sign-in via the Zed hosted flow, registered through the shared provider registry + executor. Regression guards: `tests/unit/zed-oauth-provider.test.ts`, `zed-import-utils.test.ts`, `zed-docker-detect.test.ts`, `mitm-handler-zed.test.ts`. VPS-validated via live operator login (Hard Rule #18). +- **fix(oauth):** the Kiro SSO-cache auto-import now **preserves the IDC region** — cross-region Amazon Q / Kiro profiles imported from the SSO cache are no longer collapsed to the default region ([#6113](https://github.com/diegosouzapw/OmniRoute/pull/6113)). Regression guard: `tests/unit/kiro-auto-import-idc-2059.test.ts`. VPS-validated via live operator login (Hard Rule #18). +- **fix(dashboard):** passthrough model aliases no longer collide when two namespaced model ids share a last segment. `enx/gpt-5.5` and `enx/codebuddy/gpt-5.5` both auto-generated the alias `gpt-5.5`, so the second model could never be added (the UI just alerted "alias already exists"). Aliases are now disambiguated deterministically — bare last segment when free, then parent-qualified (`codebuddy-gpt-5.5`), then a numeric suffix — while re-adding the exact same model id is still blocked. Regression guard: `tests/unit/passthrough-alias-1850.test.ts`. (thanks @arpicato) +- **fix(translator):** preserve a Gemini `functionResponse` co-located with other parts (another `functionCall`, or trailing `text`) in the same content when translating **Gemini → OpenAI**. `convertGeminiContent()` early-returned the tool message on the first `functionResponse` part, dropping any co-located parts; such contents are now pre-split (one tool message per `functionResponse`, emitted first, plus one message for the remaining parts). Regression guard: `tests/unit/gemini-to-openai-function-response.test.ts`. (thanks @warelik) +- **fix(headroom):** detect a python interpreter managed by **mise / pyenv / asdf / conda**. Headroom's python probe (`src/lib/headroom/detect.ts`) searched a hardcoded `PATH`, but version managers expose their interpreters via shim dirs that only join `PATH` through interactive-shell activation — which the non-interactive server never runs, so a managed python (≥3.10) was invisible and Headroom reported it missing. The search path now prepends the well-known shim/bin dirs (`~/.local/share/mise/shims`, `~/.pyenv/shims`, `~/.asdf/shims`, `$CONDA_PREFIX/bin`, `~/.local/bin`, respecting `MISE_DATA_DIR`/`PYENV_ROOT`/`ASDF_DATA_DIR` when set), and a new `HEADROOM_PYTHON` env override lets operators point straight at their interpreter (mirroring `HEADROOM_URL`). Still shell-free (`execFileSync`). Regression guard: `tests/unit/headroom-detect.test.ts` (5). (thanks @loopyd) +- **fix(executors):** strip the OpenAI-Codex/Claude-CLI `client_metadata` passthrough field for **NVIDIA** requests. NVIDIA's OpenAI-compatible wrapper rejects it with `400 Unsupported parameter`, the same class already handled for `cerebras`/`mistral`; `nvidia` (executor `default`) was missing from the strip allowlist so Codex/Claude-Code passthrough requests 400'd. Regression guard: `tests/unit/executor-default-strip-client-metadata.test.ts` (+nvidia case). (thanks @phidinhmanh) +- **fix(translator):** strip the Claude-style `thinking` field for **NVIDIA `z-ai/glm-5.2`**. NVIDIA's OpenAI-compatible wrapper 400s on `thinking` (a Claude-format client routed here leaves a `thinking:{type:"adaptive"}`); the existing strip rule only dropped `reasoning`. Same class already handled for `minimax-m2.7`. Regression guard: `tests/unit/nvidia-minimax-thinking-strip.test.ts` (+glm-5.2 case). (thanks @phidinhmanh) +- **fix(translator):** suppress the streamed `` close marker for the **Antigravity IDE** client. On thinking-only turns Antigravity rendered a bare `` as the sole visible content, tripping its loop-detection and wasting requests. Antigravity's UA (`vscode/ (Antigravity/)`) is added to the marker-suppress allowlist (alongside OpenCode); Claude Code / Cursor still get the marker, and `x-omniroute-thinking-marker: on` force-restores it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts`. (thanks @abdofallah) +- **fix(executors):** strip nested `reasoning_content` from messages for **Mistral**. Mistral's API returns `422 extra_forbidden` when an assistant message carries `reasoning_content` (replayed thinking from a prior turn, e.g. via the Codex `/responses` path); the generic top-level 400 field-downgrade retry never covered the nested per-message field. `DefaultExecutor` now strips it for provider `mistral` only, so DeepSeek (which requires replayed `reasoning_content`) is unaffected. Regression guard: `tests/unit/mistral-strip-reasoning-content-1649.test.ts`. (thanks @xxy9468615) +- **fix(executors):** strip the `client_metadata` passthrough field on the **OpenCode** path. OpenCode upstreams (e.g. `kimi-k2.6` via opencode-go) reject it with `400 "Extra inputs are not permitted, field: 'client_metadata'"`; the DefaultExecutor strip only covered cerebras/mistral and `OpencodeExecutor` extends `BaseExecutor` directly, so nothing removed it there. Regression guard: `tests/unit/opencode-strip-client-metadata-1442.test.ts`. (thanks @yanpaing007) +- **fix(executors):** inject the `reasoning_content` echo for the native **Moonshot Kimi** provider. Kimi (executor `default`) is a thinking-mode upstream that 400s with "reasoning_content must be passed back" when a prior assistant turn lacks it; the placeholder injection was only wired into the OpenCode meta-provider, so direct multi-turn Kimi conversations failed. Scoped to `kimi` (gateway-served models matching the thinking-model name pattern are unaffected). Regression guard: `tests/unit/kimi-native-reasoning-injected-1480.test.ts`. (thanks @2220258345) +- **fix(executors):** recover from a strict gateway's `context_management: Extra inputs are not permitted` 400. **Claude Code** always sends a top-level `context_management` field; strict anthropic-compatible gateways reject it. The dedicated context-editing 400-fallback only fired when OmniRoute's own `contextEditing` feature was enabled (default off), so a client-sent field passed through untouched and 400'd. `context_management` is now in the generic reactive field-strip list, so it's stripped-and-retried once regardless of the feature flag (with correct request re-signing for claude-compatible relays). Regression guard: `tests/unit/provider-field-strips.test.ts`. (thanks @ohahe52-dot) +- **fix(network):** enable **RFC 8305 Happy Eyeballs** (`autoSelectFamily`) on the direct-egress undici dispatcher. When DNS returns both IPv6 (AAAA) and IPv4 (A) and the IPv6 route is broken (e.g. a NAT64 `64:ff9b::` prefix without routing), undici tried IPv6 first and hung until `ETIMEDOUT` (then a 502 + account lockout), even though `curl` reached the same host. The direct dispatcher now races both families and uses whichever connects first. Proxy paths pin family via `proxyTls` and are unaffected. Regression guard: `tests/unit/direct-dispatcher-pipelining-4580.test.ts`. (thanks @adentdk) +- **fix(combo):** round-robin now advances the rotation pointer past the model that **actually served**, not the eagerly-scheduled one. With `stickyLimit: 1` (true round-robin), when the scheduled model failed and a *different* model served via fallback, the counter had already advanced +1 from the scheduled index — so the next request reused the fallback-served model, degrading round-robin into hot-spotting on whichever model was healthy. The pointer now advances to the served index + 1 (mirroring the sticky-limit>1 path). Session-stickiness (#3825) and distribution are preserved. Regression guard: `tests/unit/combo-rr-fallback-advance-948.test.ts`. (thanks @binsarjr) +- **fix(sse):** a non-string `model` field is now rejected with a `400` before the resolver, instead of crashing downstream `.toLowerCase()`/`.split()` calls into an empty-body `500` that escapes the error sanitizer ([#6407](https://github.com/diegosouzapw/OmniRoute/issues/6407)). Regression guard: `tests/unit/chat-non-string-model-6407.test.ts`. (thanks @chirag127) +- **fix(api):** unknown `/api/*` routes now return a JSON `404` (instead of the dashboard HTML shell) and scalar chat params (`model`/`temperature`/etc.) are validated **before** the provider lookup so malformed requests fail fast with a clear `400` ([#6424](https://github.com/diegosouzapw/OmniRoute/issues/6424), [#6412](https://github.com/diegosouzapw/OmniRoute/issues/6412)). Regression guards: `tests/unit/api/api-catchall-json-404.test.ts`, `tests/unit/chat-early-schema-validation-6412.test.ts`. (thanks @chirag127) +- **fix(api):** `/v1/chat/completions` now rejects a non-JSON `Content-Type` with a `400` before parsing the body ([#6414](https://github.com/diegosouzapw/OmniRoute/issues/6414)). Regression guard: `tests/unit/v1-chat-completions-content-type-6414.test.ts`. (thanks @chirag127) +- **fix(api):** the `X-OmniRoute-Compression` response header is now echoed on `/v1/chat/completions` and `/v1/completions` ([#6422](https://github.com/diegosouzapw/OmniRoute/issues/6422)). Regression guard: `tests/unit/compression-header-echo-6422.test.ts`. (thanks @chirag127) +- **fix(api):** concurrent `GET /v1/models` requests are coalesced into a single catalog build ([#6408](https://github.com/diegosouzapw/OmniRoute/issues/6408)). Regression guard: `tests/unit/v1-models-concurrent-6408.test.ts`. (thanks @chirag127) +- **fix(api):** `/v1/completions` now echoes the requested `body.model` in its JSON + streamed responses. Regression guard: `tests/unit/completions-body-model-echo.test.ts`. (thanks @chirag127) +- **fix(api):** env-var master keys now see the full `/v1/models` catalog ([#6406](https://github.com/diegosouzapw/OmniRoute/issues/6406)). Regression guard: `tests/unit/models-catalog-envkey-6406.test.ts`. (thanks @chirag127) +- **fix(api):** non-streaming `/v1/completions` responses now echo `body.model` aligned with the `X-OmniRoute-Model` header ([#6426](https://github.com/diegosouzapw/OmniRoute/issues/6426)). Regression guard: `tests/unit/v1-completions-model-header-match-6426.test.ts`. (thanks @chirag127) +- **fix(api):** unknown `/v1/*` routes now return a JSON `404 not_found` instead of the Next.js dashboard HTML shell ([#6405](https://github.com/diegosouzapw/OmniRoute/issues/6405)). Regression guard: `tests/unit/api/v1-catchall-json-404.test.ts`. (thanks @chirag127) +- **fix(api):** the per-connection provider models route now degrades to the shipped catalog when a provider's `/models` endpoint answers with a redirect ([#6267](https://github.com/diegosouzapw/OmniRoute/issues/6267)) — a `qwen-web` import failed with a raw `Redirect blocked … (307)` 503. `safeOutboundFetch` throws `REDIRECT_BLOCKED` on the 307, `getSafeOutboundFetchErrorStatus` maps it to 503, and `buildDiscoveryErrorFallbackResponse` treated every 503 as a hard error — so the non-empty `getModelsByProviderId("qwen-web")` catalog was never surfaced. A models-endpoint redirect is not a fixable-config error (unlike `URL_GUARD_BLOCKED`/`INVALID_URL`, which stay hard errors), so it now falls back to the local/cached catalog before the 503 short-circuit. General fix — covers any config-driven provider that 307s. Regression guard: `tests/unit/provider-models-qwen-web-redirect-6267.test.ts`. (thanks @chirag127) +- **fix(api):** the per-connection provider models route (MCP `list_models_catalog` + the dashboard import view) now merges USER-ADDED custom models into its response ([#6247](https://github.com/diegosouzapw/OmniRoute/issues/6247)) — custom models live in the `key_value` namespace `customModels`, which the live REST `/api/v1/models` already merges, but `src/app/api/providers/[id]/models/route.ts` never read `getCustomModels`, so custom models were dropped on both the discovery-success and local_catalog paths. They are now folded into the returned model list (deduped by id, stamped `owned_by: provider`), fixing MCP + the dashboard import view in one place. Regression guard: `tests/unit/provider-models-custom-merge-6247.test.ts`. (thanks @RCrushMe) + +- **fix(providers):** GitLab Duo tool-calling follow-up turns no longer fail upstream with `422 {"detail":"Validation error"}` (tokens 0/0, rejected pre-inference). The #6234 tool-result-feedback fix serialized the **entire** multi-turn conversation into GitLab's single-file `code_suggestions` (`small_file`) generation endpoint — folded history that turn-N sent as an oversized `current_file.content_above_cursor` **and** duplicated verbatim into `user_instruction`, tripping the AI-Gateway's `small_file` validation guard. The executor now **bounds** that prompt: it keeps system + latest user message + the most-recent tool round (dropping older turns), caps oversized tool results, and stops duplicating the full prompt into `user_instruction` (which now carries only the short latest user message) — while still feeding the most-recent tool result back so the agent continues (`open-sse/executors/gitlab.ts`, [#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)). The unit test covers the bounding logic; the upstream 422→200 clearing is VPS-only (Hard Rule #18). Regression guard: `tests/unit/gitlab-tool-exchange-bounded-6220.test.ts`. + +- **fix(i18n):** the provider-detail (`/dashboard/providers/[id]`) connection-status filter labels no longer render as `__MISSING__:All` / `__MISSING__:Active` / `__MISSING__:Error` / `__MISSING__:Banned` / `__MISSING__:CreditsExhausted` in non-English locales (notably pt-BR) ([#6290](https://github.com/diegosouzapw/OmniRoute/issues/6290)). Root cause was **not** the namespace mismatch the issue guessed — the `providers.filter*` keys resolve correctly in `en.json`; the debt lived in the locale mirrors (`src/i18n/messages/*.json`), where these five keys carried the `__MISSING__:` sync sentinel in ~15 locales and were absent entirely in ~26 others, so next-intl found the key and echoed the sentinel verbatim. All 40 non-English/-Chinese mirrors now ship real translations for the five `providers.filter*` labels. Regression guard: `tests/unit/i18n-provider-filter-keys-6290.test.ts`. (thanks @diegosouzapw) + +- **fix(providers):** the `copilot-m365-web` streaming executor now emits `debug`-level WebSocket diagnostics ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)) — the outbound WS URL (with the `access_token` **redacted** via `redactWsUrl()`), handshake success/failure, and each received SignalR frame's `type`/`target`. Previously the streaming path logged nothing, so an empty `content:null` response (the M365 Education / Starter tier symptom fixed in #6234) was undiagnosable even at `APP_LOG_LEVEL=debug`. The change is debug-level and side-effect-free — it does not alter streaming behavior or the frame parser, and the token never reaches the logs. Regression guard: `tests/unit/copilot-m365-web-logging-6210.test.ts` (thanks @qpeyba) + +- **fix(resilience):** a round-robin combo no longer returns `503 all upstream accounts are unavailable` when a compatibility-rejected target is actually healthy ([#6238](https://github.com/diegosouzapw/OmniRoute/issues/6238)). `filterTargetsByRequestCompatibility` drops request-incompatible targets (tool/vision/structured-output unsupported, or below the required context window) **before** any availability check runs, and its `compatible.length === 0` safety net only fired when *all* targets were filtered — not when the kept targets later all turned out runtime-unavailable (circuit-open / cooldown / no credentials). So a combo could 503 while a compat-rejected-but-healthy provider sat unused. `handleRoundRobinCombo` now keeps the compat-rejected set and, when every compat-kept target was skipped without a single real attempt, probes those rejected targets as a **last-resort fallback tier** (via the new pure `open-sse/services/combo/comboCompatFallback.ts`) before crystallizing the 503. Regression guard: `tests/unit/combo-roundrobin-compat-fallback-6238.test.ts`. (thanks @ThongAccount) + +- **fix(startup):** best-effort self-heal for a corrupted Turbopack dev cache on Windows ([#6289](https://github.com/diegosouzapw/OmniRoute/issues/6289)). On Windows, `pnpm dev` can fail at startup when Turbopack `mmap`s a persistent-cache SST file and the OS refuses the mapping (`os error 1455` — "paging file too small"), which Turbopack surfaces as a misleading `Module not found: Can't resolve '@/shared/utils/machine'`. This is a **known upstream Turbopack cache-corruption bug — not our code**. The dev launcher (`scripts/dev/run-next.mjs`) now wraps `nextApp.prepare()` and, when it rejects with that signature (`isTurbopackCacheCorruption` in the new `scripts/dev/turbopackCacheHeal.mjs`), purges `.build/next/**/cache/turbopack` and retries **once** with a clear log. **Caveat — best-effort only:** the corruption often surfaces as a runtime overlay rather than a `prepare()` rejection, so this cannot always intercept it; the reliable remedy remains manually deleting the Turbopack cache dir. Regression guard: `tests/unit/turbopack-cache-heal-6289.test.ts`. (thanks @chirag127) + +- **fix(providers):** qodercli PAT auth no longer fails with `spawn qodercli ENOENT` on Windows ([#6263](https://github.com/diegosouzapw/OmniRoute/issues/6263)) — `spawnQoderCli` spawned the bare `qodercli` name with `shell:false` and an unenriched env, so the npm `.cmd` wrapper under `%APPDATA%\npm` (a user-PATH directory) was never resolved. It now resolves the absolute `.cmd`/`.exe` path through the existing `getCliRuntimeStatus("qoder")` resolver in `src/shared/services/cliRuntime.ts` (memoized), spawns with `shell` when the target is a `.cmd`/`.bat`, and uses the cliRuntime-enriched env (PATH + PATHEXT + APPDATA); the ENOENT error now lists the searched paths plus the `CLI_QODER_BIN` override. End-to-end spawn on a real Windows host is host-only (Hard Rule #18); the path-resolution logic is unit-tested. Regression guard: `tests/unit/qodercli-windows-resolve-6263.test.ts`. (thanks @chirag127) + +- **fix(sse):** the reasoning-token buffer no longer inflates **probe-sized `max_tokens`** ([#6274](https://github.com/diegosouzapw/OmniRoute/issues/6274)) — Claude Code's `/model` capability check sends `max_tokens: 1`, but for a thinking-capable model with a large output cap (e.g. `glm-5.2`) the #3587 headroom heuristic (`max(current + 1000, ceil(current * 1.5))`) rewrote it to `1001` and forwarded that upstream, wasting tokens on a request that was never a genuine reasoning budget. `resolveReasoningBufferedMaxTokens()` (`open-sse/services/reasoningTokenBuffer.ts`) now short-circuits and returns the caller's value verbatim when it is below the new `REASONING_BUFFER_MIN_TRIGGER` (256) threshold — a tiny explicit limit is a probe, not a reasoning request. Real budgets still receive the #3587 headroom unchanged, and the guard runs after the existing capability checks so unknown / non-reasoning models keep returning `null`. Regression guard: `tests/unit/reasoning-token-buffer-6274.test.ts`. (thanks @brightfiscalband) + +- **fix(cli):** `omniroute reset-password` now works as a real subcommand, and password resets over piped (non-TTY) stdin actually apply ([#6261](https://github.com/diegosouzapw/OmniRoute/issues/6261), [#6258](https://github.com/diegosouzapw/OmniRoute/issues/6258)). Two coupled defects: (1) **#6261** — `bin/omniroute.mjs` routed everything through Commander with only two pre-Commander bypasses (`--mcp`, `reset-encrypted-columns`), so `omniroute reset-password` was rejected as an unknown command; only the separate `omniroute-reset-password` bin worked, while the docs falsely advertised the subcommand (incl. a bogus "legacy alias still works"). A pre-Commander bypass mirroring `reset-encrypted-columns` now dynamically imports `bin/reset-password.mjs` (which self-executes) before Commander parses; the three doc lines were corrected. (2) **#6258** — `bin/reset-password.mjs` issued two sequential `rl.question` prompts; under piped stdin the second read never settled at EOF, so `main()` never reached `resetManagementPassword` and the reset was a silent no-op (both prompts printed, no success, password unchanged). The CLI now detects non-TTY stdin and reads it once (first line = password, second line = confirm if present, else reused), adds a `--password-stdin` flag (entire stdin is the password, no confirmation), and exits `0` explicitly so the success line always flushes; interactive TTY behavior is unchanged. Regression guard: `tests/unit/reset-password-cli-6261-6258.test.ts` (3). (thanks @chirag127) + +- **fix(db):** the mass-migration **safety abort** now tells the operator how to bypass it and stops flooding the log ([#6260](https://github.com/diegosouzapw/OmniRoute/issues/6260)) — after restoring a backup that wiped the migration tracking table, `runMigrations()` threw the abort on every downstream `ensureDbInitialized()`, re-logging the full banner 11+ times, and the message never mentioned the existing `OMNIROUTE_MAX_PENDING_MIGRATIONS` escape hatch. The abort text now appends a bypass hint (set `OMNIROUTE_MAX_PENDING_MIGRATIONS=0` in `server.env` / `DATA_DIR/.env`), and a new `MigrationSafetyAbortError` is memoized so repeated calls in the same process throw the same instance and emit a single concise line instead of the full cascade. Regression guard: `tests/unit/migration-safety-abort-6260.test.ts`. (thanks @chirag127) + +- **fix(auth):** importing a **distinct** Codex/ChatGPT OAuth `auth.json` is no longer falsely rejected as "already exists" when it belongs to a different user in the same workspace ([#6301](https://github.com/diegosouzapw/OmniRoute/issues/6301)). `findExistingCodexConnection` (in `src/lib/oauth/utils/codexAuthImport.ts`) deduped **only** on `providerSpecificData.workspaceId === accountId`, where `accountId` is the shared `chatgpt_account_id`/`tokens.account_id` — so two members of the same ChatGPT Team collapsed onto a single connection (409 `duplicate_account`). The id_token's `https://api.openai.com/auth` claim carries a per-user `chatgpt_user_id` alongside the workspace id (the device-flow path already persisted it as `chatgptUserId`, but the import path did not). Now `parseAndValidateCodexAuth` extracts `userId` (`chatgpt_user_id` → `user_id` → JWT `sub`) into `ParsedCodexAuth`, the create/update paths persist `chatgptUserId` in `providerSpecificData` (mirroring `codex.ts`), and dedup keys on `workspaceId` **AND** `chatgptUserId` — with a backward-compat fallback to legacy accountId-only matching when no stored connection for that workspace records a `chatgptUserId`, so genuinely-same accounts still dedup. Regression guard: `tests/unit/codex-auth-import-userid-dedup-6301.test.ts` (4). (thanks @anungma) + +- **fix(providers):** importing models for the **venice-web** provider no longer fails with a red "Provider venice-web does not support models listing" ([#6269](https://github.com/diegosouzapw/OmniRoute/issues/6269)). `venice-web` is a web-cookie provider with an executor but no upstream `/v1/models` endpoint and no registry `models`, so the models route fell through to the tail `400`. Mirroring the `jules`/`linkup-search`/`ollama-search` fix (#5569), it now ships a static local catalog entry in `src/lib/providers/staticModels.ts` — seeding the current Venice lineup (`venice-uncensored`, `llama-3.3-70b`, `qwen3-235b`, `qwen3-4b`, `deepseek-r1-671b`; Venice rotates its catalog, see docs.venice.ai/models/overview) — so the route returns `200` with `source:"local_catalog"`, `intentional:true`. Regression guard: `tests/unit/static-models-venice-web-6269.test.ts`. (thanks @chirag127) + +- **fix(api):** the specialty model catalogs (`/v1/embeddings`, `/v1/images`, `/v1/music`, `/v1/videos` model lists) are now derived from the **unified catalog filtered by a predicate** (`getSpecialtyModelsResponse`) instead of ad-hoc per-route logic, so they consistently respect active-credential visibility and stay in sync with the main catalog. Regression guard: `tests/unit/specialty-model-catalog-routes.test.ts`. (thanks @makcimbx) +- **fix(api):** the agent-bridge server route now resolves the MITM manager via a **dynamic `import("@/mitm/manager.runtime")`** so Turbopack does not statically pull the stub (or over-bundle the manager), and the agent-skills generator anchors its output base path with `path.join(process.cwd(), …)` so Turbopack's static analyzer stops tracing the whole project root ([#6329](https://github.com/diegosouzapw/OmniRoute/issues/6329)). Regression guard: `tests/unit/agent-bridge-server-route-dynamic-import.test.ts`. (thanks @Iammilansoni) +- **fix(api):** internal probes (combo-test, cloud-sync verify) now pick a **management-scoped / allow-all API key** instead of naively grabbing `getApiKeys()[0]` — a restricted `self:usage` first row made the probe fail with "Model X is not allowed for this API key" even when the combo path was healthy (`pickApiKeyForInternalUse` in `src/lib/db/apiKeys.ts`). The API-manager model editor also falls back to `/api/models?all=true` when `/v1/models` is catalog-protected. Regression guard: `tests/unit/pick-internal-api-key-6372.test.ts`. (thanks @jmengit) +- **fix(live-ws):** the Live Dashboard WebSocket server now **rejects on bind failure** (e.g. `EADDRINUSE` when the API bridge already holds the port) instead of letting the error surface as an unhandled `error` event that crash-loops the process — the `error` listener is attached to `wss` (not `server`) and releases the EventBus subscription on a failed start ([#6324](https://github.com/diegosouzapw/OmniRoute/issues/6324)). Regression guard: `tests/unit/live-ws-eaddrinuse-6324.test.ts`. (thanks @vinayakkulkarni) +- **fix(dashboard):** the Home provider-topology widget now trusts the live provider-metrics snapshot — it uses `topology.errorProvider` and live `activeRequests` directly instead of re-deriving state from a stale `lastErrorAt` or applying a frontend timeout filter, so the topology reflects real-time provider health. Regression guard: `tests/unit/home-provider-topology-live-state.test.ts`. (thanks @xz-dev) +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(sse):** tool-call function schemas with a root `type: null` are now coerced to `type: "object"` before dispatch ([#6359](https://github.com/diegosouzapw/OmniRoute/issues/6359)) — clients like the Codex app emit `parameters: { type: null, ... }` for some tools, which OpenAI-compatible upstreams reject with `400 Invalid schema for function '...': schema must be a JSON Schema of 'type: "object"', got 'type: null'`, failing the whole request. `toolSchemaSanitizer` already stripped the null; it now re-adds the mandatory root `"object"` type (and empty `properties`/open `additionalProperties` when absent). Combinator roots (`anyOf`/`oneOf`/`allOf`) and explicit root types are left untouched. Regression guard: 5 new cases in `tests/unit/tool-schema-sanitizer.test.mjs`. + +- **fix(docker):** AgentBridge no longer fails to start on npm/Electron/VPS installs with "MITM manager stub reached at runtime" ([#6344](https://github.com/diegosouzapw/OmniRoute/issues/6344)) — v3.8.45 flipped the production bundler default to Turbopack, but `next.config.mjs` aliased `@/mitm/manager` to its Docker-only degraded stub **unconditionally**. That was harmless while Docker (which sets the alias intentionally for #3390 graceful degradation) was the sole Turbopack consumer, but once every artifact built with Turbopack the stub shipped to all non-Docker users and `startMitm` threw on the first Agent-Bridge start. The alias is now opt-in via `OMNIROUTE_MITM_STUB=1` (set only by the Dockerfile) through the shared `scripts/build/mitm-stub-flag.mjs` helper; default builds bundle the real manager. Regression guard: `tests/unit/mitm-stub-alias-6344.test.mjs` (4). + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + - **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | + +--- + +## [3.8.44] — 2026-07-04 + +### ✨ New Features + +- **feat(resilience):** throttle upstream quota fetches on the per-request preflight path ([#6009](https://github.com/diegosouzapw/OmniRoute/issues/6009)) — a new global min-interval gate (`open-sse/services/quotaFetchThrottle.ts`) spaces the actual network calls made by the Codex quota fetcher so that many accounts on one IP no longer fetch quota in the same second (which, per `router-for-me/CLIProxyAPI#2385`, can get a Codex OAuth token revoked). Complements the existing bulk-sync spacing (`PROVIDER_LIMITS_SYNC_SPACING_MS`) which already serialized the periodic provider-limits sync — this covers the concurrent combo/preflight path it didn't. Cache hits are never delayed; fail-open (only ever awaits a timer). Configurable via `OMNIROUTE_QUOTA_FETCH_MIN_INTERVAL_MS` (default 250ms, clamped 0..5000; `0` disables). Regression guard: `tests/unit/quota-fetch-throttle-6009.test.ts` (5). (thanks @powellnorma) +- **feat(autoCombo):** add **per-request Auto-Combo controls** via two headers ([#6024](https://github.com/diegosouzapw/OmniRoute/issues/6024) / [#6025](https://github.com/diegosouzapw/OmniRoute/issues/6025) / [#6023](https://github.com/diegosouzapw/OmniRoute/issues/6023)) — `X-OmniRoute-Mode` steers an `auto` combo's scoring for a single request (friendly presets `fast`/`balanced`/`quality`/`cheap`/`reliable`/`offline` **or** a raw mode-pack name; `balanced` forces the default weights), and `X-OmniRoute-Budget` sets a hard per-request USD cost ceiling. Both override the combo's stored config only for the request that carries them; unknown/garbage values are ignored so the saved config is preserved. The resolvers are pure (`open-sse/services/autoCombo/requestControls.ts`) and feed the engine's existing `config.modePack` / `config.budgetCap` inputs — no engine changes. Regression guard: `tests/unit/auto-combo-request-controls-6024.test.ts` (5). (thanks @chirag127) +- **feat(providers):** add the **Kenari** OpenAI-compatible gateway (BYOK). Regression guard: `tests/unit/kenari.test.ts`. (thanks @doedja) +- **feat(models):** add `claude-sonnet-5` to the Antigravity model catalog (alias mapping in `antigravityModelAliases.ts`) ([#6103](https://github.com/diegosouzapw/OmniRoute/pull/6103)). Regression guard: `tests/unit/antigravity-model-aliases.test.ts`. (thanks @anki1kr) +- **feat(api):** add `/v1/ocr` endpoint (Mistral OCR), an OCR provider category, and Mistral moderation support. ([#5950](https://github.com/diegosouzapw/OmniRoute/pull/5950)) (thanks @waguriagentic) +- **Discovery tool (Phase 2):** add the `discoveryResults` DB module (CRUD over the `discovery_results` table, migration 074) and wire the opt-in provider-discovery service to persist and read findings through it (`persistDiscoveryResult`, `getDiscoveryResults`, `getDiscoveryResultById`, `markVerified`, `deleteDiscoveryResult`) with `(provider, method, endpoint)` upsert de-duplication. Adds the `/api/discovery/*` HTTP surface — `GET /results`, `GET|DELETE /results/:id`, `POST /scan`, `POST /verify/:id` — under **strict loopback-only** authorization (`/api/discovery/` is in `LOCAL_ONLY_API_PREFIXES` and is NOT manage-scope-bypassable, so the `scan` route's outbound probes can never be reached from a tunnel/remote origin). Adds a **dashboard UI tab** (Tools → Discovery, `/dashboard/discovery`) to run scans and review, verify, or delete findings. The service stays **opt-in / default-off**. ([#5939](https://github.com/diegosouzapw/OmniRoute/pull/5939)) +- **feat(api):** expose a read-only provider plugin manifest at `GET /api/v1/provider-plugin-manifest` for sidecar/relay discovery. ([#6001](https://github.com/diegosouzapw/OmniRoute/pull/6001)) (thanks @KooshaPari) +- **feat(sidecar):** advertise the provider manifest URL to Bifrost/CLIProxyAPI via the `X-OmniRoute-Provider-Manifest-Url` header (`OMNIROUTE_PROVIDER_MANIFEST_URL`). ([#6007](https://github.com/diegosouzapw/OmniRoute/pull/6007)) (thanks @KooshaPari) +- **feat(autoCombo):** add a latency/speed-optimized routing mode (shared `rankBySpeed` scoring core) plus the `omniroute_pick_fastest_model` MCP tool. ([#6011](https://github.com/diegosouzapw/OmniRoute/pull/6011)) (thanks @KooshaPari) +- **feat(providers):** refresh The Old LLM (Free) model catalog ([#5181](https://github.com/diegosouzapw/OmniRoute/issues/5181)) — seed the current free `/api/chatgpt` tier (GPT-5/5.1/5.2/5.3/5.4, o3/o4-mini, Gemini 3 Pro / 2.5 Pro / 2.0 Flash / 1.5 Flash, Claude 4.6 Opus/Sonnet & 4.5 Haiku, GPT-4o, Grok 4, DeepSeek V3/R1, Sonar Pro) while keeping the legacy alias IDs for saved-preference compatibility. Also fixes a latent routing bug: `mapModel()` now passes known upstream IDs through unchanged, so Gemini/o-series/Grok/DeepSeek/Sonar models no longer silently collapse onto `GPT_5_4`. Regression guard: `tests/unit/theoldllm-model-refresh-5181.test.ts`. (thanks @WslzGmzs) +- **feat(resilience):** surface Codex **banked reset credits** per connected account ([#5199](https://github.com/diegosouzapw/OmniRoute/issues/5199)) — the Codex quota parsers (`buildCodexUsageQuotas`, `parseCodexUsageResponse`) now additively read `rate_limit_reset_credits.available_count` (+ optional `rate_limit_reached_type`) from the `/wham/usage` payload OmniRoute already fetches, and the provider-limits dashboard renders a **"Banked Reset Credits"** row when a positive count is present. Display-only and **fail-open** — the field is eligibility-gated, so accounts without it are unaffected (parsers never throw on absent/garbage shapes); redemption (an unofficial mutating endpoint) is intentionally out of scope. Regression guard: `tests/unit/codex-banked-reset-credits-5199.test.ts` (8). (thanks @ofekbetzalel) +- **feat(providers):** add sign-up geo-restriction notices for **SenseNova** and **StepFun** ([#5462](https://github.com/diegosouzapw/OmniRoute/issues/5462)) — the provider add-form now warns that SenseNova's console appears to require a Chinese (+86) phone number with no documented international path, and that StepFun's default endpoint is its China platform while a global StepFun Open Platform (`platform.stepfun.ai`, operated by Sparkling AI Pte. Ltd., Singapore) with email/Google/Discord login exists for international users. Informational `notice` only — neither provider is disabled. Regression guard: `tests/unit/regional-provider-cn-notices-5462.test.ts`. (thanks @chirag127) +- **feat(usage):** add on-demand period-scoped usage-data reset (Settings → System Storage) with a purge API and time-window selector. ([#5831](https://github.com/diegosouzapw/OmniRoute/pull/5831)) +- **feat(claude-code):** add an opt-in auto-permission classifier compat mode (off/auto/always) for Claude Code, toggleable from the CLI Code settings. ([#5810](https://github.com/diegosouzapw/OmniRoute/pull/5810)) +- **feat(providers):** add optional client-identity header profiles for compatible nodes — preset User-Agent/fingerprint headers (e.g. matching a known CLI) merged into the existing customHeaders field. ([#5812](https://github.com/diegosouzapw/OmniRoute/pull/5812)) +- **feat(build):** add a backend-only fast build mode (`scripts/build/build-next-isolated.mjs` + `backendOnlyPages.mjs`) that skips compiling the dashboard frontend pages, cutting local/CI build time for backend-only changes. ([#6119](https://github.com/diegosouzapw/OmniRoute/pull/6119) — thanks @artickc) +- **feat(minimax):** extract MiniMax M3's raw `...` leakage into `reasoning_content` on the 8 OpenAI-format provider tiers, leaving the Claude-format `minimax`/`minimax-cn` tiers untouched (they already report reasoning correctly). ([#6073](https://github.com/diegosouzapw/OmniRoute/pull/6073) — thanks @KooshaPari) +- **feat(services):** promote **Bifrost** (`@maximhq/bifrost` — Go AI-gateway) from an env-only relay sidecar to a first-class embedded/supervised service, matching the existing cliproxy/9router model — installer, bootstrap `SERVICES[]` entry, migration 113 DB seed, 7 lifecycle API routes under `/api/services/bifrost/` (loopback-only), a dashboard tab, and relay auto-wiring that defaults `BIFROST_BASE_URL` to the supervised port when running. Implements item #2 of #5670; the broader RouterBackend contract (items #1, #3-#5) stays out of scope. ([#5817](https://github.com/diegosouzapw/OmniRoute/pull/5817), part of [#5670](https://github.com/diegosouzapw/OmniRoute/issues/5670)) +- **feat(services):** add **Mux** (`coder/mux` — local agent-orchestration daemon) as a fourth-tier embedded service on the existing `ServiceSupervisor` framework — npm-based installer, `bootstrap.ts` registration, migration 113 DB seed, 7 lifecycle API routes under `/api/services/mux/` (loopback-only, defense-in-depth bind to 127.0.0.1), and a dashboard tab reusing the shared service-management components. ([#6034](https://github.com/diegosouzapw/OmniRoute/pull/6034)) +- **feat(xai):** surface Grok/xAI usage on the quota dashboard via local `usageHistory` aggregation (`getXaiUsage`) — since xAI exposes no per-account quota API, this sums tokens routed to the connection from `usage_history` and reports them as a cumulative, uncapped quota, mirroring the existing Xiaomi MiMo self-track pattern. ([#5806](https://github.com/diegosouzapw/OmniRoute/pull/5806)) +- **feat(minimax):** extract MiniMax M3's raw `...` tags into a separate `reasoning_content` field on the 8 provider tiers that register M3 with `format:"openai"` (trae, huggingchat, bazaarlink, ollama-cloud, opencode, cline, opencode-zen, codebuddy-cn) — previously the thinking text leaked directly into `content`. Reuses the existing `extractThinkingFromContent` primitive, extending its allowlist with a minimax-m3-only pattern; the two direct minimax/minimax-cn tiers are untouched since they already surface reasoning natively over Anthropic's Messages format. (Inspired by 9router#2231.) ([#6050](https://github.com/diegosouzapw/OmniRoute/pull/6050) — thanks @KooshaPari) +- **feat(i18n):** auto-detect the browser language on first visit — a pure `detectBrowserLocale()` matcher (exact match, `zh-HK`/`zh-MO` folded to `zh-TW`, language-prefix match, else `null`) plus a client-only `LocaleAutoDetect` component mounted once in the root layout. When no locale cookie is set yet, it reads `navigator.languages`, computes a match against the supported locales, and persists it via the same cookie/localStorage writer `LanguageSelector` already used (extracted to `shared/lib/persistLocale.ts`). (Inspired by 9router#1324.) ([#5979](https://github.com/diegosouzapw/OmniRoute/pull/5979)) +- **feat(cli-tools):** add **CodeWhale** — the actively-maintained successor to DeepSeek TUI (same author, renamed project) — as a dual dashboard entry alongside the existing "deepseek-tui" catalog entry, so existing DeepSeek TUI users keep a working card while new users are steered to CodeWhale. New `/api/cli-tools/codewhale-settings` route writes `~/.codewhale/config.toml` and keeps the legacy `~/.deepseek/config.toml` in sync. (Inspired by 9router#1761.) ([#5996](https://github.com/diegosouzapw/OmniRoute/pull/5996)) +- **feat(server):** support reverse-proxy `basePath` deployment via a new opt-in `OMNIROUTE_BASE_PATH` env var (empty by default), using Next.js's native `basePath` support so a deployment behind a reverse-proxy subpath (e.g. `https://host/omniroute/`) works without manual header stripping; the two hardcoded auth-redirect targets in `src/server/authz/pipeline.ts` now prefix with `request.nextUrl.basePath`. Default empty basePath is a no-op for existing root-path deployments. (Inspired by 9router#1810.) ([#5992](https://github.com/diegosouzapw/OmniRoute/pull/5992)) +- **feat(providers):** add **SumoPod** (`ai.sumopod.com`) and **X5Lab** (`api.x5lab.dev`) OpenAI-compatible BYOK aggregator gateways, wired via the default executor with bearer API-key auth; both use `passthroughModels` with a live `/v1/models` fetcher instead of a hardcoded catalog. Regression guard: `tests/unit/sumopod-x5lab-provider.test.ts`. (Inspired by 9router#1288.) ([#5963](https://github.com/diegosouzapw/OmniRoute/pull/5963)) +- **feat(providers):** add **Charm Hyper** (`hyper.charm.land`) as a new OpenAI-compatible, bearer-auth API-key gateway provider with a free tier (100 monthly Hypercredits); models resolve via passthrough (`modelsUrl` + live `/v1/models`) since the catalog isn't publicly documented. (Inspired by 9router#2006.) ([#5961](https://github.com/diegosouzapw/OmniRoute/pull/5961)) +- **feat(providers):** add **Nube.sh** (`ai.nube.sh`) as a new BYOK OpenAI-compatible gateway (LiteLLM proxy), Bearer/API-key auth. Its live model catalog is only reachable with a valid key, so no model IDs are hardcoded — it uses `passthroughModels` + `modelsUrl` for live enumeration. (Inspired by 9router#2294.) ([#5936](https://github.com/diegosouzapw/OmniRoute/pull/5936) — thanks @whale9820) +- **feat(providers):** add **b.ai** (`api.b.ai`) as a new OpenAI-compatible BYOK provider, distinct from the existing thebai/theb.ai provider, using passthrough model discovery with no hardcoded model list. (Inspired by 9router#963.) ([#5969](https://github.com/diegosouzapw/OmniRoute/pull/5969)) +- **feat(providers):** add **Qiniu** (七牛云) AI inference gateway as a BYOK API-key provider — proxies many upstream models (DeepSeek V3/V4, Claude, Kimi, and more) behind a single key, shipping with an empty static seed and relying on `passthroughModels` + the live `/v1/models` catalog instead of a stale hardcoded model id. Regression guard: `tests/unit/qiniu-provider.test.ts`. (Inspired by 9router#911.) ([#5966](https://github.com/diegosouzapw/OmniRoute/pull/5966)) +- **feat(providers):** port **ModelScope** (Alibaba 魔搭) as a new API-key, OpenAI-compatible provider — verified against ModelScope's own docs that the real production domain is `api-inference.modelscope.cn` (`.cn`, not the upstream PR's `.ai`) and shipped `passthroughModels: true` with an empty seed + `modelsUrl` instead of the upstream PR's static 5-model snapshot, since the open-model catalog moves fast. (Ported from 9router#1764.) ([#5965](https://github.com/diegosouzapw/OmniRoute/pull/5965) — thanks @tn5052) +- **feat(providers):** add **Augment (Auggie CLI)** as a new local, no-auth provider that spawns the user's local `auggie` CLI and pipes a flattened prompt via stdin, wrapping stdout as an OpenAI-compatible SSE stream or single JSON body. Auth is delegated to `auggie login` outside OmniRoute (synthetic `noAuth: true` connection, no DB row required); "Test Connection" spawns `auggie --version`. Hardened against the untrusted-input spawn sink: no `shell: true` on Windows (argv passed straight to the OS loader, no metacharacter interpretation), and `model` is validated against the registry allowlist before spawn (rejecting unknown or `-`-prefixed values) with a trailing `--` end-of-options marker. (Inspired by 9router#1200.) ([#5972](https://github.com/diegosouzapw/OmniRoute/pull/5972) — thanks @chamdanilukman) +- **feat(providers):** add **NVIDIA NIM image generation** — a dedicated `nvidia-nim` image format/handler (separate host, `ai.api.nvidia.com/v1/genai/`, native NIM body shape) for the 4 FLUX models (flux.1-dev, flux.1-schnell, flux.1-kontext-dev, flux.2-klein-4b), shaping each model's per-model request body (dimension/mode validation, required input image + aspect ratio, optional edit image) and normalizing the NIM response's varying shapes into the OpenAI `{created, data}` shape. (Inspired by 9router#1195.) ([#5971](https://github.com/diegosouzapw/OmniRoute/pull/5971)) +- **feat(oauth):** import a Codex connection from a raw ChatGPT access token — OmniRoute's only Codex import path previously required both `access_token` and `refresh_token`, leaving no path for a user with only a bare ChatGPT website access token. `createProviderConnection` gains an explicit `access_token` auth-type branch (intentionally never deduped), a new `POST /api/oauth/codex/import-token` route (Zod-validated), and `OAuthModal`'s manual-paste path now detects an `eyJ`-prefixed pasted token and posts it to the new endpoint, mirroring the existing grok-cli raw-token flow. The executor's `refreshCredentials()` already degrades safely to `null` without a refresh token, forcing re-auth on expiry. (Inspired by 9router#1290.) ([#5995](https://github.com/diegosouzapw/OmniRoute/pull/5995) — thanks @ryanngit) +- **feat(dashboard):** add a tool-source diagnostics settings toggle — a new Settings → Advanced card lets operators flip the existing `logToolSources` flag from the UI instead of editing the DB row directly; `logToolSources` is added to the `.strict()` `/api/settings` Zod PATCH schema (previously rejected). (Inspired by 9router#1825.) ([#5978](https://github.com/diegosouzapw/OmniRoute/pull/5978) — thanks @DuyPrX) +- **feat(dashboard):** collapse and sort provider quota rows by remaining percentage — the expanded quota list is sorted highest-remaining-first and collapsed to the first 3 rows by default, with a "Show N more"/"Show less" toggle when a connection reports more than 3 quotas, keeping at-risk quotas visible above a long list of healthy ones. Sort/slice logic extracted into pure, directly-unit-tested helpers (`sortQuotasByRemaining`, `getVisibleQuotas`). (Inspired by 9router#1919.) ([#5977](https://github.com/diegosouzapw/OmniRoute/pull/5977)) +- **feat(dashboard):** suggest HuggingFace Hub media models — a new `GET /api/v1/providers/suggested-models` route proxies the public HF Hub models search API (Zod-validated, no token exposed client-side) and `ImageExampleCard` merges the results into the model picker as a selectable chip row for the huggingface provider; also adds a dedicated `huggingface-image` format/handler for HF's raw-image-bytes response. (Inspired by 9router#1633.) ([#5990](https://github.com/diegosouzapw/OmniRoute/pull/5990)) +- **feat(cli-tools):** add a **Crush** entry to the dashboard CLI-Tools catalog plus a new `/api/cli-tools/crush-settings` route (GET/POST/DELETE) — OmniRoute already shipped a `crush` CLI setup command (`bin/cli/commands/setup-crush.mjs`) but the dashboard catalog had no matching entry; the new route writes to the same canonical `~/.config/crush/crush.json` path so the dashboard and CLI command agree. (Inspired by 9router#1233.) ([#5970](https://github.com/diegosouzapw/OmniRoute/pull/5970)) +- **feat(providers):** extend Vercel AI Gateway (`vercel-ai-gateway`/`vag`) beyond chat-only to support **embeddings and image generation** — the gateway's OpenAI-compatible `/v1` API also exposes `/embeddings` and `/images/generations`, so entries were added to `EMBEDDING_PROVIDERS` (`embeddingRegistry.ts`) and `IMAGE_PROVIDERS` (`imageRegistry.ts`) modeled on the existing `openai` entries. ([#5968](https://github.com/diegosouzapw/OmniRoute/pull/5968) — thanks @tantai-newnol) +- **feat(api-keys):** add per-key **device/connection tracking** — a SHA-256 fingerprint of IP + User-Agent, with a 30-minute TTL and per-key/global caps, tracks distinct client devices seen with each API key (in-memory only, raw IP never stored). A new `GET /api/keys/[id]/devices` route exposes masked device details, and the API Keys dashboard tab gets a "Devices" count badge alongside the existing Sessions badge. This is a new granularity distinct from the existing `maxSessions` cap, which limits concurrent sticky-routing sessions rather than tracking device identity. ([#5998](https://github.com/diegosouzapw/OmniRoute/pull/5998) — thanks @mugni-rukita) +- **feat(proxy):** add **Webshare** (`proxy.webshare.io`) as a fourth source in the free-proxy provider framework alongside 1proxy, Proxifly, and IPLocate. `WebshareProvider` paginates the account's `/api/v2/proxy/list/` endpoint, upserts proxies into the shared `free_proxies` table, and tombstones proxies the account no longer lists while never touching rows already promoted into the live proxy pool. Unlike the other sources, Webshare is a paid per-account list, gated on `FREE_PROXY_WEBSHARE_API_KEY`. ([#5993](https://github.com/diegosouzapw/OmniRoute/pull/5993) — thanks @ricatix) +- **feat(antigravity):** support custom **Google Cloud project ID** settings from the connection edit modal (Antigravity family). ([#5905](https://github.com/diegosouzapw/OmniRoute/pull/5905) — thanks @nickwizard) +- **feat(dashboard):** add a **wildcard-CORS runtime warning** banner (Settings → Authorization) when `CORS_ALLOW_ALL`/`*` origins are in effect, plus a new `docs/security/CORS.md` security guide covering the risk and safer alternatives. ([#5602](https://github.com/diegosouzapw/OmniRoute/issues/5602), [#5759](https://github.com/diegosouzapw/OmniRoute/pull/5759)) +- **feat(api):** add a `/v1/audio/translations` endpoint (Whisper-style audio translation), a new `audioTranslation` handler, and translation providers wired into `audioRegistry`. Regression guard: `tests/unit/audio-translations-route.test.ts` (8, incl. no-stack-leak). ([#5809](https://github.com/diegosouzapw/OmniRoute/pull/5809)) +- **feat(providers):** allow a **custom icon URL** for compatible provider nodes (migration 113 + `nodes.ts` + Zod schema + API routes + catalog + `ProviderIcon` UI). Regression guards: 14 backend + 5 frontend(vitest) + 24 page-utils tests. ([#5815](https://github.com/diegosouzapw/OmniRoute/pull/5815)) +- **feat(xai):** register a dedicated `XaiExecutor` with reasoning-effort suffix parsing. Regression guard: `tests/unit/executors/xai-executor.test.ts` (6). ([#5800](https://github.com/diegosouzapw/OmniRoute/pull/5800)) +- **feat(webfetch):** support **self-hosted FireCrawl** instances via `FIRECRAWL_BASE_URL`/`FIRECRAWL_TIMEOUT_MS`. Regression guard: `tests/unit/executors/firecrawl-fetch.test.ts` (4). ([#5793](https://github.com/diegosouzapw/OmniRoute/pull/5793)) +- **feat(providers):** add **ClinePass** as a first-class API-key (BYOK) provider — Cline's paid gateway (`cline-pass/*` models, plain Bearer key), distinct from the existing OAuth `cline` provider. Regression guard: 16 clinepass tests. ([#5942](https://github.com/diegosouzapw/OmniRoute/pull/5942) — thanks @adentdk) +- **feat(relay):** gate **Bifrost auto-routing** by the provider plugin manifest — only manifest-eligible providers reach the sidecar; ineligible/unknown providers fall back to the existing TS routing path with explicit reasons. Regression guards: 4 provider-plugin-manifest + 11 relay-routing-backend tests. ([#5870](https://github.com/diegosouzapw/OmniRoute/pull/5870) — thanks @KooshaPari) +- **feat(providers):** wire **Claude Sonnet 5** end-to-end across the model pipeline — registries, `modelSpecs`, pricing (×3), cost, Sonnet-family fallback, 1M-context, and static models. ([#5833](https://github.com/diegosouzapw/OmniRoute/pull/5833) — thanks @ggiak) + +### 🔧 Bug Fixes + +- **dashboard (`/dashboard/system/proxy` 500 on every render):** `ProxyRegistryManager` called `useProxyBatchOperations(load)` before the `const load = useCallback(...)` declaration in the component body, so every server render threw a TDZ `ReferenceError: Cannot access 'load' before initialization` and the whole proxy page 500'd (#5918 regression, caught by the release-PR e2e smoke — the PR→release fast-gates never render pages). The hook block now sits after the `load` declaration. Regression guard: `tests/unit/ui/ProxyRegistryManager-tdz-render.test.tsx` (SSR renderToString — the exact crash mode). + +- **server (TRACE/TRACK/CONNECT returned raw 500 on every route):** methods that undici/fetch cannot represent blew up inside Next's middleware adapter (`TypeError: 'TRACE' HTTP method is unsupported.`) as an unhandled 500 (caught by the release-PR dast-smoke Schemathesis negative tests on the new `/api/keys/{id}/devices` endpoint). The raw HTTP method guard now answers a clean 405 + `Allow` header for these methods on any path, before Next sees the request. Regression guard: `tests/unit/dast-method-not-allowed.test.ts` (new case). + +- **i18n (auto-detect refreshed every first visit):** `LocaleAutoDetect` (#5979) called `router.refresh()` on every cookie-less first visit — even when the detected browser locale was exactly the one the server had just rendered — re-navigating the page mid-interaction (flaky e2e "execution context destroyed" + a visible flash for every new visitor). It now refreshes only when the detected locale differs from the server-rendered ``. Regression guard: `tests/unit/ui/LocaleAutoDetect-refresh.test.tsx`. + +- **models (`oc/` alias must reach the no-auth OpenCode provider):** restore the [#2901](https://github.com/diegosouzapw/OmniRoute/issues/2901) routing contract after the #5918 transitive-alias change made the registered no-auth `opencode` provider unreachable by any prefix (`oc/` chained through the manual `opencode` → `opencode-zen` slug override and misrouted its combo entries). `resolveProviderAlias` now stops the alias chain as soon as a hop lands on a registered provider id, while keeping #5918's transitivity across alias-only hops and its loop/depth guards. Regression guards: `tests/unit/combo-builder-opencode-prefix.test.ts`, `tests/unit/provider-alias-transitive-5918.test.ts`. + +- **providers (Auggie executor EPIPE crash):** a fast-exiting `auggie` CLI (e.g. binary present but immediately failing) delivered `EPIPE` **asynchronously** as an `'error'` event on the child's stdin stream — which a plain try/catch around `stdin.write()` cannot catch — crashing the request instead of surfacing the sanitized CLI error. Both spawn sites now attach a stdin `'error'` handler so the child's own exit/close handlers report the failure. Regression guard: `tests/unit/auggie-executor.test.ts` (deterministic 3/3 locally). + +- **dashboard (CoolingConnectionsPanel broke `next build`):** the cooling-connections panel from #6061 imported `Card` from a shadcn-style path that does not exist in this repo (`@/components/ui/card`) and pulled the server DB barrel (`@/lib/localDb`) into a client component — `next build` failed to compile on the release branch. The panel now renders with repo-native markup and reads `formatResetCountdown` from the new client-safe `src/shared/utils/formatting.ts`. Regression guards: `tests/unit/format-reset-countdown.test.ts`, `tests/unit/ui/CoolingConnectionsPanel.test.tsx`. ([#6155](https://github.com/diegosouzapw/OmniRoute/pull/6155)) + +- **oauth (Zed "Unknown provider" crash):** adding **Zed** from the providers dashboard threw an unhandled `OAuth GET error: Unknown provider: zed` (500) ([#6041](https://github.com/diegosouzapw/OmniRoute/issues/6041)). Zed is a **keychain-import-only** provider — it's listed in the OAuth catalog so the UI shows it, but has no OAuth handler, so the generic `/api/oauth/[provider]/[action]` route hit `getProvider("zed")` and crashed. The route now recognizes keychain-import-only providers and returns a clear **400** pointing users at the **Import** button (for both GET and POST OAuth actions), instead of a 500. Regression guard: `tests/unit/oauth-keychain-import-only-6041.test.ts`. (thanks @imblowsnow) + +- **fix(providers):** disable the unsupported `thinking` param for `minimax-m2.7` on NVIDIA NIM (the upstream rejects it) ([#6102](https://github.com/diegosouzapw/OmniRoute/pull/6102)). Regression guard: `tests/unit/nvidia-minimax-thinking-strip.test.ts`. (thanks @anki1kr) + +- **fix(mitm):** add an in-process guard so concurrent MITM server starts no longer race — a second start while one is already in flight is short-circuited instead of double-binding the listener ([#6107](https://github.com/diegosouzapw/OmniRoute/pull/6107)). Regression guard: `tests/unit/mitm-start-guard.test.ts`. (thanks @anki1kr) + +- **translator (Responses → Chat Completions):** strip the Responses-API-only `truncation` field before forwarding a `/v1/responses` request to a non-OpenAI Chat Completions upstream ([#6109](https://github.com/diegosouzapw/OmniRoute/pull/6109)). Strict upstreams (e.g. NVIDIA NIM) rejected it with HTTP 400 `Unsupported parameter(s): truncation`, breaking Codex-style clients routed to those providers. `client_metadata`, `background`, and `safety_identifier` were already stripped — `truncation` was the remaining gap. Regression guard: `tests/unit/responses-strip-truncation-2311.test.ts`. (thanks @TuanNguyen0708) + +- **combo (prefer known context capacity over unknown):** when a combo filters out at least one target for exceeding a _known_ context limit, the router now prefers the remaining known-compatible targets over targets whose context metadata is simply unknown, instead of letting unknown-metadata targets be the only survivors. If no known-compatible context target remains, context-only candidates fall back to the normal strategy order. Regression guard: `tests/unit/combo-context-window-filter.test.ts`. ([#6088](https://github.com/diegosouzapw/OmniRoute/pull/6088) — thanks @Thinkscape) + +- **models (GLM-5.2 context normalization):** stop treating every hosted GLM-5.2 provider alias as the native 1M-context model. Native/bare GLM-5.2 and verified OpenCode / ZenMux routes keep their 1,000,000-token context, while hosted-provider aliases now respect the caps declared in their provider metadata instead of inheriting the native max. Regression guards: `tests/unit/model-capabilities-registry.test.ts`, `tests/unit/models-catalog-route.test.ts`. ([#6091](https://github.com/diegosouzapw/OmniRoute/pull/6091) — thanks @Thinkscape) + +- **providers (Gemini Web):** refresh the Gemini Web cookie handling and model catalog so live Gemini Web sessions keep authenticating and routing to current models. Regression guard: `tests/unit/gemini-web.test.ts`. ([#6095](https://github.com/diegosouzapw/OmniRoute/pull/6095) — thanks @backryun) + +- **providers (Perplexity Web):** refresh the Perplexity Web model catalog to the current set (GPT-5.4/5.5, Claude Sonnet 5.0 / Opus 4.8, GLM-5.2, Kimi K2.6, Nemotron 3 Ultra) and update the internal mode / `model_preference` mappings and thinking variants so requests resolve to live upstream models. Regression guard: `tests/unit/perplexity-web.test.ts`. ([#6106](https://github.com/diegosouzapw/OmniRoute/pull/6106) — thanks @backryun) + +- **dashboard ("Update now" → Internal Server Error):** clicking **Update now** on the dashboard home could crash the page with a blank "Internal Server Error" screen (`Minified React error #31`). The handler POSTs the loopback-only `/api/system/version` auto-update endpoint and, on a non-OK JSON response (e.g. a `403` when the dashboard is reached through a reverse proxy / non-loopback origin), passed the raw error envelope object `{ error: { code, message, correlation_id } }` straight to `notify.error()`, which rendered the object as a React child and threw #31. The update-error path now funnels the body through `extractApiErrorMessage()` (the same safe extractor added in #5340), so a readable string always reaches the toast. Regression guard: `tests/unit/ui/home-update-error-render-5991.test.ts`. ([#5991](https://github.com/diegosouzapw/OmniRoute/issues/5991)) +- **fix(onboarding):** route the provider-details link in the onboarding wizard by the node's stable id instead of the composite provider slug, which could point at the wrong provider details page for multi-account/fingerprint nodes. Regression guard: `tests/unit/onboarding-wizard-details-link-6145.test.ts`. ([#6145](https://github.com/diegosouzapw/OmniRoute/pull/6145) — thanks @chirag127) +- **fix(cli):** give `setup-claude` a fallback profile generator mirroring `setup-codex`, so profile generation no longer silently no-ops when the primary generator path is unavailable. Regression guard: `tests/unit/cli/setup-claude.test.ts` (new cases). ([#6138](https://github.com/diegosouzapw/OmniRoute/pull/6138) — thanks @derhornspieler) +- **fix(glm):** suppress a leaked `` close marker in the GLM Anthropic transport, which was surfacing the raw reasoning-close tag in visible response content instead of being consumed as part of the thinking-block framing. Regression guard: `tests/unit/glm-think-close-marker-leak.test.ts`. ([#6133](https://github.com/diegosouzapw/OmniRoute/pull/6133) — thanks @dhaern) +- **fix(provider-limits):** close a TOCTOU race in quota-recovery clearing by moving the check-then-clear to a CAS (compare-and-swap) primitive in `src/lib/db/providers.ts`, so two concurrent recovery paths can no longer both observe stale state and double-clear/re-lock a connection. Regression guard: `tests/unit/provider-limits-recovery.test.ts`. ([#6139](https://github.com/diegosouzapw/OmniRoute/pull/6139) — thanks @janeza2) +- **fix(provider-limits):** clear transient rate-limit state (`rateLimitedUntil`, `lastError`, `backoffLevel`) as soon as quota recovers, instead of leaving stale rate-limit fields behind that could keep a now-healthy connection looking unavailable. Regression guard: `tests/unit/provider-limits-recovery.test.ts`. ([#6128](https://github.com/diegosouzapw/OmniRoute/pull/6128) — thanks @janeza2) +- **combos (OpenCode/MiMo fingerprint accounts):** expand fingerprint-scoped OpenCode/MiMo accounts into their full per-fingerprint set in the combo builder, which previously showed only the first matching account entry and hid the rest from combo target selection. Regression guard: `tests/unit/combo-builder-fingerprint-expansion.test.ts`. ([#6092](https://github.com/diegosouzapw/OmniRoute/pull/6092), closes [#6087](https://github.com/diegosouzapw/OmniRoute/issues/6087) — thanks @anki1kr) +- **fix(auth):** persist quota-preflight account lockouts until the reset window elapses, instead of losing the lockout on process restart and letting a still-quota-exhausted account be selected again immediately. Regression guards: `tests/unit/sse-auth.test.ts`, `tests/unit/opencode-quota-fetcher.test.ts`, `tests/unit/usage-service-hardening.test.ts`. ([#6090](https://github.com/diegosouzapw/OmniRoute/pull/6090) — thanks @Thinkscape) +- **combo (fingerprint-based provider expansion):** expand fingerprint-based providers into per-fingerprint combo targets (`open-sse/services/combo/fingerprintExpansion.ts`) so a combo referencing a fingerprint-scoped provider fans out to every matching fingerprint account instead of collapsing onto one. Regression guards: `tests/unit/combo-fingerprint-expansion.test.ts`, `tests/integration/fingerprint-expansion.test.ts`. ([#6082](https://github.com/diegosouzapw/OmniRoute/pull/6082) — thanks @pizzav-xyz) +- **fix (safety-net redirect `reqId` crash):** fix a `reqId` `ReferenceError` thrown inside the safety-net combo redirect path in `src/sse/handlers/chat.ts`, remove dead code in `src/domain/quotaCache.ts`, and rename the stray root `DESING.md` to `DESIGN.md`. Regression guard: `tests/unit/chat-safetynet-reqid-6097.test.ts`. ([#6097](https://github.com/diegosouzapw/OmniRoute/pull/6097) — thanks @fix2015) +- **fix(compression):** send a patch-only body to `PUT /api/settings/compression` from `CompressionHub`, instead of round-tripping the full settings object and risking clobbering fields changed elsewhere between load and save. Regression guard: `tests/unit/ui/CompressionHub-patch-only.test.tsx`. ([#6077](https://github.com/diegosouzapw/OmniRoute/pull/6077), closes [#6039](https://github.com/diegosouzapw/OmniRoute/issues/6039) — thanks @anki1kr) +- **fix(codex):** use `access_token.exp` instead of `id_token.exp` when computing `expiresAt` on Codex auth import, since the `id_token` can expire far sooner than the actual access token, causing imported connections to be treated as expired while still usable. Regression guard: `tests/unit/codex-auth-import-expiry.test.ts`. ([#6084](https://github.com/diegosouzapw/OmniRoute/pull/6084), closes [#6075](https://github.com/diegosouzapw/OmniRoute/issues/6075) — thanks @anki1kr) +- **fix(security):** persist the IP allow/block-list configuration (it was resetting to Disabled and clearing configured IPs on every restart/update) and actually enforce it in the authz pipeline (`src/server/authz/pipeline.ts`), where it was previously validated but never applied. Regression guards: `tests/unit/ip-filter-persistence-6131.test.ts`, `tests/unit/authz/ip-filter-enforcement-6131.test.ts`, `tests/unit/ip-filter.test.ts`. (closes [#6131](https://github.com/diegosouzapw/OmniRoute/issues/6131), [#6132](https://github.com/diegosouzapw/OmniRoute/pull/6132)) +- **fix (Claude tool_result adjacency):** reattach an OpenAI-shaped `tool_result` to sit directly adjacent to its originating `tool_use` before translating to Claude's message format (`open-sse/translator/request/openai-to-claude/toolResultAdjacency.ts`), since Claude's API rejects/mishandles a tool result separated from its tool call by intervening messages. Regression guard: `tests/unit/translator-openai-to-claude.test.ts` (new cases). ([#6035](https://github.com/diegosouzapw/OmniRoute/pull/6035) — thanks @KooshaPari) +- **fix(config):** externalize `ws`/`bufferutil`/`utf-8-validate` in `next.config.mjs` so the `copilot-m365-web` executor's WebSocket masking path works at runtime — chat requests through it were silently timing out because the bundler was inlining `ws` instead of leaving it as a real Node dependency. Regression guard: `tests/unit/next-config.test.ts`. ([#6130](https://github.com/diegosouzapw/OmniRoute/pull/6130), closes [#6062](https://github.com/diegosouzapw/OmniRoute/issues/6062) — thanks @anki1kr, whose #6098 fix it re-lands) +- **fix(registry):** update grok-cli model context lengths to match the actual Grok CLI `/context` capacities — `grok-build` 128k→256k, `grok-composer-2.5-fast` 128k→200k — so context-aware routing stops filtering these models out for exceeding a stale, too-low limit. Registry-only. ([#5913](https://github.com/diegosouzapw/OmniRoute/pull/5913) — thanks @Chewji9875) +- **fix(providers):** strip an orphan `tool_result` (one with no preceding `tool_use`) on the Antigravity MITM path before translating to OpenAI format, since an unpaired tool result upstream caused request failures. Regression guard: `tests/unit/antigravity-orphan-toolresult-6026.test.ts`. (closes [#6026](https://github.com/diegosouzapw/OmniRoute/issues/6026), [#6115](https://github.com/diegosouzapw/OmniRoute/pull/6115)) +- **fix(providers):** emulate OpenAI-style `tool_calls` in the GitLab Duo executor (new `open-sse/executors/gitlabResponses.ts`), since the executor previously didn't emulate tool-call semantics for Duo, breaking tool-using clients routed to GitLab Duo. Regression guard: `tests/unit/gitlab-duo-toolcalls-6051.test.ts`. (closes [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051), [#6111](https://github.com/diegosouzapw/OmniRoute/pull/6111)) +- **fix(429 / accountFallback):** persist the per-account 429 cooldown cascade across the request boundary and classify OpenCode's "Monthly usage limit. Resets in N days." message as a connection-scoped quota exhaustion with an N-day cooldown (instead of a ~5s transient retry), so an exhausted account stops being re-selected until its window resets. ([#6061](https://github.com/diegosouzapw/OmniRoute/pull/6061) — thanks @KooshaPari / @anki1kr, whose superseded #6086 carried the same day-parser approach) +- **combo (sibling-model fallback on per-model-quota 500s):** when a combo held multiple models from the same provider (e.g. two Gemini models) and the first returned a server 500, the router retried the same locked model and surfaced a 429 "cooling down" instead of trying the sibling — `markConnectionLevelExhaustion` was wrongly tripped by a model-level 500 for per-model-quota providers (gemini, github, passthrough, compatible), and the retry loop didn't check `isModelLocked` before re-hitting the same model. Both gaps are fixed; the combo now falls through to the untried sibling model. Regression guard: `tests/unit/combo/combo-target-exhaustion.test.ts` (21 cases). ([#5976](https://github.com/diegosouzapw/OmniRoute/pull/5976) — thanks @hartmark) +- **providers (Cline non-streaming envelope):** Cline can return OpenAI-compatible chat completions wrapped as `{ success, data: { choices, usage, ... } }`; the non-streaming path checked the top-level body for empty content before unwrapping, so a valid wrapped response could be misclassified as malformed/empty. The envelope is now unwrapped immediately after provider-envelope handling, before empty-content detection, usage extraction, and translation. Regression guard: `tests/unit/cline-response-envelope.test.ts`. ([#6046](https://github.com/diegosouzapw/OmniRoute/pull/6046) — thanks @KooshaPari) +- **providers (kimi-web, qwen-web):** align the kimi-web model catalog and request-scenario selection with `www.kimi.com`'s live `GetAvailableModels` response, and stop aliasing `qwen3-coder-plus` on qwen-web now that it is present as its own model in the live Qwen web catalog. ([#5915](https://github.com/diegosouzapw/OmniRoute/pull/5915) — thanks @janeza2) +- **translator (Antigravity/Gemini tool schemas):** strip `multipleOf` from function-declaration parameters before forwarding to Antigravity/Gemini — it is not part of the Gemini OpenAPI 3.0 schema subset accepted upstream and triggered a hard 400 ("Unknown name multipleOf"). Added to `GEMINI_UNSUPPORTED_SCHEMA_KEYS` so it is stripped at every schema level; `minimum`/`maximum` are unaffected since Gemini accepts them. (Ported from 9router#2309, reported by @abil0321.) ([#6052](https://github.com/diegosouzapw/OmniRoute/pull/6052)) +- **translator (Kiro system prompt leak):** Kiro/CodeWhisperer has no system role, so system messages were normalized into a bare user turn — the full Claude Code system prompt then appeared as raw user text, polluting model context. System-origin content is now wrapped in `` tags before merging into the Kiro user message; real user turns are unaffected. (Ported from 9router#2306, reported by @VitzS7.) ([#6053](https://github.com/diegosouzapw/OmniRoute/pull/6053)) +- **fix(codex):** convert Chat Completions `json_schema` `response_format` → Responses API `text.format` on the Codex path, and preserve an existing `text.format` through verbosity normalization. Regression guards: 48 translator-openai-responses-req + 8 codex-verbosity tests. ([#5933](https://github.com/diegosouzapw/OmniRoute/pull/5933) — thanks @yusufrahadika) +- **fix(thinking):** only inject the `redacted_thinking` replay block when `tool_use` is present and thinking is enabled, avoiding a fabricated replay block on plain (non-tool) turns. ([#5945](https://github.com/diegosouzapw/OmniRoute/issues/5945), [#5953](https://github.com/diegosouzapw/OmniRoute/pull/5953)) +- **fix(resilience):** honor active **codex session affinity** over per-request reset-aware re-scoring, so an in-flight session sticks to its pinned account instead of being re-scored away mid-conversation. New `src/sse/services/sessionAffinityPin.ts` module. Regression guard: `tests/unit/codex-session-affinity-reset-aware-5903.test.ts`. ([#5903](https://github.com/diegosouzapw/OmniRoute/issues/5903), [#5943](https://github.com/diegosouzapw/OmniRoute/pull/5943)) +- **fix(resilience):** compute per-window `is_exhausted` and honor the quota-exhaustion preflight for **priority combos**, so a combo no longer keeps routing to a target whose current window is already exhausted. New `open-sse/services/combo/quotaExhaustionCutoff.ts`. Regression guard: `tests/unit/combo-priority-quota-exhaustion-cutoff-5923.test.ts`. ([#5923](https://github.com/diegosouzapw/OmniRoute/issues/5923), [#5941](https://github.com/diegosouzapw/OmniRoute/pull/5941)) +- **fix(providers):** strip a `/v1` suffix from the base URL unconditionally in both models-discovery paths, avoiding a doubled `/v1/v1/models` fetch error (e.g. Api Airforce). Regression guard: `tests/unit/airforce-v1-double-prefix-5899.test.ts`. ([#5899](https://github.com/diegosouzapw/OmniRoute/issues/5899), [#5920](https://github.com/diegosouzapw/OmniRoute/pull/5920) — thanks @anki1kr) +- **fix(api):** relax provider-scoped chat completion validation on `/api/providers/[provider]/chat/completions`. Regression guard: `tests/unit/provider-scoped-chat-completions-validation.test.ts`. ([#5907](https://github.com/diegosouzapw/OmniRoute/pull/5907) — thanks @nickwizard) +- **fix(providers):** validate **v0 Platform** (Vercel) API keys via the `/chats` endpoint instead of a probe that rejected valid keys. Regression guard: `tests/unit/provider-validation-specialty.test.ts`. ([#5954](https://github.com/diegosouzapw/OmniRoute/pull/5954) — thanks @vittoroliveira-dev) +- **fix(mcp):** auto-recover stale streamable HTTP MCP sessions on `initialize` instead of failing the reconnect. Regression guard: `tests/unit/mcp-session-sweep.test.ts`. ([#5957](https://github.com/diegosouzapw/OmniRoute/pull/5957) — thanks @Chewji9875) +- **fix(translator):** enforce strict Anthropic content-block compliance when converting an antigravity → openai request. Regression guard: `tests/unit/translator-antigravity-to-openai.test.ts` (9). ([#5935](https://github.com/diegosouzapw/OmniRoute/pull/5935)) +- **fix(sse):** strip ANSI/VT100 escape codes from `gemini-cli` stream frames using a ReDoS-safe pattern. Regression guard: `tests/unit/gemini-cli-ansi-sanitization.test.ts` (5). ([#5934](https://github.com/diegosouzapw/OmniRoute/pull/5934) — thanks @anki1kr) +- **fix(discovery):** resolve a doubled `/v1` discovery path and a `REDIRECT_BLOCKED` probe-loop abort in the model-discovery route. Regression guard: `tests/unit/provider-models-route.test.ts`. ([#5904](https://github.com/diegosouzapw/OmniRoute/pull/5904) — thanks @hamsa0x7) +- **fix(providers): Perplexity Web now emits real `tool_calls` in streaming mode** — previously only non-streaming requests (`hasTools && !stream`) converted `{...}` text into OpenAI `tool_calls`; streaming requests (the default for agentic coding clients) got the raw `` text as plain `delta.content` and never emitted a `tool_calls` SSE delta. Now mirrors the `chatgpt-web` `toolMode` helpers (`buildToolModeResponse()`/`toolCompletionToSseStream()`, extended with a caller-supplied `idSeed` so tool-call ids stay provider-specific), buffering the completion and emitting a terminal SSE replay carrying `delta.tool_calls` + `finish_reason: tool_calls` regardless of the caller's stream flag. ([#5927](https://github.com/diegosouzapw/OmniRoute/issues/5927), [#5937](https://github.com/diegosouzapw/OmniRoute/pull/5937)) +- **providers (openai-family model inference no longer hijacks cataloged models):** `resolveModelByProviderInference()` had an unconditional `/^gpt-/i` heuristic that hijacked any model id starting with `gpt-`/`o1`/`o3` into provider `openai`, even when the id is cataloged under other providers — breaking bare (non-combo) requests for open-weight models like `gpt-oss-120b` (served by fireworks/cerebras/scaleway/byteplus/sambanova/heroku), which don't exist on openai's catalog, producing a 404 with no fallback. The heuristic is now gated on `providers.length === 0` so it only fires for genuinely uncataloged openai-family ids. Regression guard: `tests/unit/gptoss-provider-inference-5852.test.ts`. ([#5852](https://github.com/diegosouzapw/OmniRoute/issues/5852), [#5938](https://github.com/diegosouzapw/OmniRoute/pull/5938)) +- **fix(providers): deepseek-web reliability** — auto-refresh the session on `401`/`403`, refresh the v2.0.0 client headers, and fix the token-kind bulk import path. Regression guards: `tests/unit/deepseek-web-autorefresh-401-response.test.ts`, `tests/unit/bulk-web-session-import.test.ts`. ([#5988](https://github.com/diegosouzapw/OmniRoute/pull/5988) — thanks @backryun) +- **fix(api):** guard the shared frontend API client (`handleResponse` in `src/shared/utils/api.ts`) against non-JSON error responses — it previously called `response.json()` unconditionally and read `data.error` directly, throwing an unrelated parse error (or `undefined`) instead of a useful message when an upstream/proxy returned a non-JSON error body. Now routes through `parseResponseBody`/`getErrorMessage` to build a safe message regardless of body shape. Regression guard: `tests/unit/shared-api-utils.test.ts`. ([#5973](https://github.com/diegosouzapw/OmniRoute/pull/5973)) +- **fix(embeddings):** forward the connection-level proxy configuration to embedding requests — `src/lib/embeddings/service.ts` previously ignored a connection's configured proxy when making embedding calls, so proxy-only network setups leaked embedding traffic outside the proxy. Regression guard: `tests/unit/embeddings-proxy-forwarding.test.ts`. ([#5975](https://github.com/diegosouzapw/OmniRoute/pull/5975)) +- **fix(resilience):** parse `Retry-After` from a 429's JSON body for cooldown calculation, not just the HTTP header — a new `retryAfterJson.ts` helper extracts a retry-after hint from common JSON error-body shapes and `accountFallback.ts`'s cooldown path now prefers it when the header is absent. Regression guard: `tests/unit/account-fallback-retry-after-json.test.ts`. (Includes #6013's retry-after-json extraction.) ([#5974](https://github.com/diegosouzapw/OmniRoute/pull/5974) — thanks @KooshaPari) + +### 📝 Maintenance + +- **release close (release-PR one-pass CI sweep):** restore Zod validation on the provider-scoped chat route with a `.passthrough()` schema that keeps #5907's relaxed semantics (t06 route-validation gate); point `/api/keys/{id}/devices`' 401 response at the management error envelope in `docs/openapi.yaml` (Schemathesis schema-conformance); rebaseline `i18nUiCoverage.pct` 77.5→76.8 (~1352 new en.json UI keys from the cycle await the async translation workflow — same shape as the v3.8.39 rebaseline); dismiss 2 CodeQL `js/incomplete-url-substring-sanitization` false positives on unit-test asserts (v3.8.35 precedent). + +- **release close (Phase 0 pre-flight):** align cycle-stale tests with merged behavior — provider count 166→167 (Kenari #6104), Linux-regenerated translate-path golden (+`kenari`), OpenCode quota scope `provider`→`connection` (#6061) — and absorb cycle ratchet drift (file-size caps for `oauth/[provider]/[action]/route.ts` 960, `providerLimits.ts` 998, `chat.ts` 1662, `auth.ts` 2426, with #6158 tracked to restore the oauth-route freeze). The test-masking gate gains a narrowly-scoped `_deletedWithReplacement` allowlist section (deletion is exempt ONLY when the declared replacement test file exists in HEAD — used for `targetExhaustion.test.ts` → `tests/unit/combo/combo-target-exhaustion.test.ts`, which has MORE coverage: 21 cases/52 asserts vs 13/37), plus 5 new gate unit tests and reduction-allowlist entries for the verified-legitimate #5958/#6088/#5816 assert migrations. + +- **test (deflake `setup-claude`):** `tests/unit/cli/setup-claude.test.ts` failed ~50% of runs with `Unable to deserialize cloned data due to invalid or unsupported version` at file teardown (all subtests passed), randomly reddening `Unit Tests fast-path (2/2)` / `Fast Quality Gates` across the PR→release queue. Root cause: `node --test` streams each file's report to the parent as V8-serialized frames on fd 1 (stdout), and the CLI helper under test (`syncClaudeProfilesFromModels`) prints progress via `console.log` — that stdout output interleaved with the serialized frames and corrupted the stream. The test now silences the stdout-writing `console` methods for the file's duration (no assertion inspects stdout), making it deterministic (15/15 green locally). ([#5959](https://github.com/diegosouzapw/OmniRoute/issues/5959)) ([#6021](https://github.com/diegosouzapw/OmniRoute/pull/6021)) + +- **API validation:** add a `validatedJsonBody(request, schema)` helper in `src/shared/validation/helpers.ts` that fuses JSON body parsing and Zod validation into a single call, returning either the type-narrowed data or a ready-to-return 400 `NextResponse` with the standard error envelope. Salvaged from the closed refactor PR #5075 (Tier 1 portable helper) with a focused 6-case regression test. Co-authored-by: KooshaPari +- **repo (Windows case-conflict cleanup):** remove the stale root `DESIGN.md`, which case-conflicted with `design.md` and broke checkouts/clones on case-insensitive Windows filesystems. ([#6140](https://github.com/diegosouzapw/OmniRoute/pull/6140) — thanks @backryun) +- **i18n(zh-CN):** translate the CHANGELOG entries and section headings, adopting zh-CN as a fully translated locale alongside the existing supporting docs. ([#6043](https://github.com/diegosouzapw/OmniRoute/pull/6043) — thanks @studyzy) +- **docs (env-doc-sync base-red):** document `BIFROST_PORT` in `.env.example` / `docs/reference/ENVIRONMENT.md` — the Bifrost embedded-service merge referenced `process.env.BIFROST_PORT` (default 8080) without documenting it, so `check:env-doc-sync` failed on the release tip and reddened Fast Quality Gates for every open PR→release. Docs-only (`8d7e3e28f`). +- **test (CI-runner-independent translate-path golden):** normalize OS/arch-derived request headers (`X-Stainless-Os`/`X-Stainless-Arch`, `(OS;arch)` User-Agent segments, and Antigravity's `os.platform()`-derived platform substring) in the provider translate-path golden snapshot, so the test no longer depends on the OS/arch of the CI runner that generated it — a Mac-literal Antigravity UA was failing on Linux CI. Regression guard: `tests/unit/provider-translate-path-golden.test.ts`. ([#6076](https://github.com/diegosouzapw/OmniRoute/pull/6076) — thanks @KooshaPari) +- **release-green base-reds (#5695 regex + file-size rebaseline):** `tests/unit/ui/quick-start-api-keys-link-5695.test.ts` now tolerates Prettier splitting a multi-line `` so the `step1Desc` regex matches the `/dashboard/api-manager` link instead of skipping to `step2`'s single-line `/dashboard/providers` link (test was brittle, not the code). Also rebaselines 5 files that grew via already-merged release-tip PRs in `config/quality/file-size-baseline.json` (`ApiManagerPageClient` 3017→3058, `OAuthModal` 969→989, `cliRuntime` 1090→1100, `webProvidersA` 805→809, `deepseek-web.test` 1081→1092), with shrink tracked in #3501. ([#6093](https://github.com/diegosouzapw/OmniRoute/pull/6093)) +- **release close (LEDGER-4 base-red):** the `cline-pass` provider's `minimax-m3` registry entry was missing `supportsVision`, breaking the LEDGER-4 registry-consistency test (every `minimax-m3` entry must set `supportsVision` to match `lite.ts` — the model is multimodal). Flagged it to match every other `minimax-m3` entry (trae, bazaarlink, cline, ollama-cloud, ...). ([#6003](https://github.com/diegosouzapw/OmniRoute/pull/6003)) +- **release close (stryker `tap.testFiles` drift):** additional release-green cleanup clearing the `qoder` registry's `minimax-m3` `supportsVision` LEDGER-4 base-red and `stryker.conf.json`'s `tap.testFiles` drift. ([#6012](https://github.com/diegosouzapw/OmniRoute/pull/6012)) +- **install (pnpm 11+ support):** pnpm 11 introduced `ERR_PNPM_IGNORED_BUILDS` for native addon packages — without explicit `allowBuilds` approval, packages silently skip their build scripts and OmniRoute fails to start with missing native modules. Sets `allowBuilds=true` for all 13 native addon packages in `pnpm-workspace.yaml` (`@parcel/watcher`, `@swc/core`, `better-sqlite3`, `core-js`, `esbuild`, `keytar`, `koffi`, `libxmljs2`, `onnxruntime-node`, `protobufjs`, `sharp`, `tls-client-node`, `unrs-resolver`) and migrates `onlyBuiltDependencies` from the deprecated `package.json` field to a new `pnpm.json`. (commit 39349da18 — thanks @chirag127) +- **refactor (Block J hot-path decomposition):** extract pure leaves with no behavior change from the executor, translator, combo, and SSE hot paths — orphaned executor tests moved to top-level so a runner collects them, and `handleComboChat`'s auto-strategy/target-timeout regions split into named helpers. ([#6063](https://github.com/diegosouzapw/OmniRoute/pull/6063), [#6049](https://github.com/diegosouzapw/OmniRoute/pull/6049), [#6036](https://github.com/diegosouzapw/OmniRoute/pull/6036), [#6030](https://github.com/diegosouzapw/OmniRoute/pull/6030), [#6020](https://github.com/diegosouzapw/OmniRoute/pull/6020), [#6018](https://github.com/diegosouzapw/OmniRoute/pull/6018), [#6017](https://github.com/diegosouzapw/OmniRoute/pull/6017), [#6016](https://github.com/diegosouzapw/OmniRoute/pull/6016), [#6015](https://github.com/diegosouzapw/OmniRoute/pull/6015), [#6014](https://github.com/diegosouzapw/OmniRoute/pull/6014), [#6008](https://github.com/diegosouzapw/OmniRoute/pull/6008), [#6006](https://github.com/diegosouzapw/OmniRoute/pull/6006), [#6000](https://github.com/diegosouzapw/OmniRoute/pull/6000), [#5999](https://github.com/diegosouzapw/OmniRoute/pull/5999), [#5994](https://github.com/diegosouzapw/OmniRoute/pull/5994), [#5967](https://github.com/diegosouzapw/OmniRoute/pull/5967), [#5962](https://github.com/diegosouzapw/OmniRoute/pull/5962), [#5960](https://github.com/diegosouzapw/OmniRoute/pull/5960), [#5947](https://github.com/diegosouzapw/OmniRoute/pull/5947), [#5949](https://github.com/diegosouzapw/OmniRoute/pull/5949), [#5940](https://github.com/diegosouzapw/OmniRoute/pull/5940), [#5932](https://github.com/diegosouzapw/OmniRoute/pull/5932)) +- **chore (quality/CI housekeeping):** rebaseline residual ESLint/cognitive-complexity/file-size drift accumulated over the v3.8.44 cycle, move orphaned executor tests to a top-level location so a runner actually collects them, harden the release pipeline with a test-masking pre-flight gate plus contributors/uncovered helpers, and make the `pr-evidence` FAIL output tell the author to push (a body edit alone does not re-run the gate). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926), [#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944), [#5952](https://github.com/diegosouzapw/OmniRoute/pull/5952), [#6027](https://github.com/diegosouzapw/OmniRoute/pull/6027), [#5928](https://github.com/diegosouzapw/OmniRoute/pull/5928), plus a #5975-collateral test hardening pinning a seeded connection to direct egress in route-edge-coverage) +- **docs (housekeeping):** normalize mixed-language documentation content, restore the OpenAPI coverage ratchet by documenting 9 newly-added routes, record Hard Rule #22 (cross-session safety — `git stash` + in-flight PR bans), and document the compression-engine's upstream sync policy for the RTK/Caveman engines. ([#6105](https://github.com/diegosouzapw/OmniRoute/pull/6105), [#5955](https://github.com/diegosouzapw/OmniRoute/pull/5955), [#5948](https://github.com/diegosouzapw/OmniRoute/pull/5948), plus docs-only commit 926b08aa8) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.44: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | +| [@adentdk](https://github.com/adentdk) | #5942 | +| [@anki1kr](https://github.com/anki1kr) | #5899, #5920, #5934, #6039, #6061, #6062, #6075, #6077, #6084, #6086, #6087, #6092, #6098, #6130 | +| [@artickc](https://github.com/artickc) | #6119 | +| [@backryun](https://github.com/backryun) | #5988, #6095, #6106, #6140 | +| [@chamdanilukman](https://github.com/chamdanilukman) | #5972 | +| [@Chewji9875](https://github.com/Chewji9875) | #5913, #5957 | +| [@chirag127](https://github.com/chirag127) | #6145 | +| [@derhornspieler](https://github.com/derhornspieler) | #6138 | +| [@dhaern](https://github.com/dhaern) | #6133 | +| [@doedja](https://github.com/doedja) | direct commit / report | +| [@DuyPrX](https://github.com/DuyPrX) | #5978 | +| [@fix2015](https://github.com/fix2015) | #6097 | +| [@ggiak](https://github.com/ggiak) | #5833 | +| [@hamsa0x7](https://github.com/hamsa0x7) | #5904 | +| [@hartmark](https://github.com/hartmark) | #5976 | +| [@imblowsnow](https://github.com/imblowsnow) | direct commit / report | +| [@janeza2](https://github.com/janeza2) | #5915, #6128, #6139 | +| [@KooshaPari](https://github.com/KooshaPari) | #5870, #5974, #6035, #6046, #6050, #6061, #6073, #6076, #6086 | +| [@mugni-rukita](https://github.com/mugni-rukita) | #5998 | +| [@nickwizard](https://github.com/nickwizard) | #5905, #5907 | +| [@ofekbetzalel](https://github.com/ofekbetzalel) | direct commit / report | +| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6082 | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@ricatix](https://github.com/ricatix) | #5993 | +| [@ryanngit](https://github.com/ryanngit) | #5995 | +| [@studyzy](https://github.com/studyzy) | #6043 | +| [@tantai-newnol](https://github.com/tantai-newnol) | #5968 | +| [@Thinkscape](https://github.com/Thinkscape) | #6088, #6090, #6091 | +| [@tn5052](https://github.com/tn5052) | #5965 | +| [@TuanNguyen0708](https://github.com/TuanNguyen0708) | direct commit / report | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #5954 | +| [@waguriagentic](https://github.com/waguriagentic) | direct commit / report | +| [@whale9820](https://github.com/whale9820) | #5936 | +| [@WslzGmzs](https://github.com/WslzGmzs) | direct commit / report | +| [@yusufrahadika](https://github.com/yusufrahadika) | #5933 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | + --- ## [3.8.43] — 2026-07-02 @@ -282,6 +743,8 @@ ### 📝 Maintenance +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) across `REPOSITORY_MAP.md`, the db-schema diagram and `llm.txt` (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167) — thanks @diegosouzapw) + - **docs (architecture):** add `docs/architecture/ROUTER_BACKENDS.md` — an ADR pinning down how the routing engines (`ts` native, `bifrost`, `cliproxy`, `9router`, VibeProxy-compatible) relate to each other along two orthogonal axes (lifecycle: in-process / supervised / external vs. relay selection backend), answering the architecture questions raised in [#5603](https://github.com/diegosouzapw/OmniRoute/issues/5603) (backend interface model, why CLIProxy spawns a process, feature-flag swapping, actionable route-contract errors). The typed router-backend registry the ADR describes lands separately via [#5868](https://github.com/diegosouzapw/OmniRoute/pull/5868). ([#5891](https://github.com/diegosouzapw/OmniRoute/pull/5891)) - **tests (autoCombo):** stabilize the `getTaskFitnessWithSource identifies fitness_table as source for known models` unit test, which flaked whenever the models.dev capabilities DB was populated in CI: the fixture model `gpt-4o` is a real models.dev catalog id, so the fitness resolution chain returned `models_dev_tier` instead of the expected static `fitness_table` source. The fixture now uses `claude-sonnet` (a shortened alias absent from the models.dev catalog, matching the sibling resolution-chain test), which deterministically falls through to the static table — the exact `source` and score assertions are preserved (`0.95` = `FITNESS_TABLE.coding["claude-sonnet"]`). ([#5890](https://github.com/diegosouzapw/OmniRoute/pull/5890)) — thanks @KooshaPari diff --git a/Dockerfile b/Dockerfile index 01f54c1bd1..81df902f13 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,8 +8,8 @@ WORKDIR /app # that already have a fix published in trixie. CVEs without an upstream fix yet # (local-only TOCTOU, etc.) remain until the distro patches them and the image # is rebuilt; none are reachable from the proxy's request surface at runtime. -RUN --mount=type=cache,target=/var/cache/apt,sharing=shared \ - --mount=type=cache,target=/var/lib/apt/lists,sharing=shared \ +RUN --mount=type=cache,id=apt-cache,target=/var/cache/apt,sharing=shared \ + --mount=type=cache,id=apt-lists,target=/var/lib/apt/lists,sharing=shared \ apt-get update \ && apt-get upgrade -y \ && apt-get install -y --no-install-recommends libsecret-1-0 ca-certificates \ @@ -29,8 +29,8 @@ FROM base AS builder # Build tools for native module compilation # apt-get update needed here because base's rm -rf clears the shared cache -RUN --mount=type=cache,target=/var/cache/apt,sharing=shared \ - --mount=type=cache,target=/var/lib/apt/lists,sharing=shared \ +RUN --mount=type=cache,id=apt-cache,target=/var/cache/apt,sharing=shared \ + --mount=type=cache,id=apt-lists,target=/var/lib/apt/lists,sharing=shared \ apt-get update \ && apt-get install -y --no-install-recommends python3 make g++ \ && rm -rf /var/lib/apt/lists/* @@ -55,32 +55,40 @@ ENV NPM_CONFIG_LEGACY_PEER_DEPS=true # are reproducible. RUN test -f package-lock.json \ || (echo "package-lock.json is required for reproducible Docker builds" >&2 && exit 1) -RUN --mount=type=cache,target=/root/.npm \ +RUN --mount=type=cache,id=npm-cache,target=/root/.npm \ npm ci --no-audit --no-fund --legacy-peer-deps --ignore-scripts \ && npm rebuild better-sqlite3 \ && node -e "require('better-sqlite3')(':memory:').close()" -# Build with webpack (stable). Turbopack hit a non-recoverable internal panic on this -# Next.js version during the v3.8.27 release build — TurbopackInternalError "entered -# unreachable code: there must be a path to a root" in ImportTracer::get_traces, on both -# linux/amd64 and linux/arm64. Webpack is the proven engine (build:release / VPS / CI Build -# all green). Re-enable Turbopack (=1) once the upstream tracer bug is fixed. +# Build with Turbopack (stable in Next 16, the repo default). The v3.8.27-era +# TurbopackInternalError panic ("entered unreachable code: there must be a path to a +# root" in ImportTracer::get_traces) no longer reproduces on Next 16.2.9 — validated +# 2026-07-05 with clean amd64 (12min14s, image smoke-tested: /api/monitoring/health +# 200) and arm64 (qemu, exit 0, zero panic strings) builds. Turbopack cut the bare +# build from 17min to 9min on the same 32-core box. Webpack stays available as the +# escape hatch: `--build-arg`/-e OMNIROUTE_USE_TURBOPACK=0. # See docs/ops/QUALITY_GATE_PLAYBOOK.md Parte 6. -ENV OMNIROUTE_USE_TURBOPACK=0 +ENV OMNIROUTE_USE_TURBOPACK=1 + +# Docker containers cannot run the MITM/Agent-Bridge stack (no host DNS/cert +# access), so keep @/mitm/manager on the graceful stub (#3390). This flag is +# Docker-only: npm/Electron/VPS builds must bundle the REAL manager (#6344). +ENV OMNIROUTE_MITM_STUB=1 # Raise the V8 heap ceiling for the build. The webpack production optimization -# pass (forced above since Turbopack panics) needs more than V8's default ceiling -# (~2 GB) for a codebase this size; a memory-constrained Docker build otherwise -# dies with "FATAL ERROR: ... JavaScript heap out of memory" during the builder -# stage (#4076). NODE_OPTIONS propagates to the spawned `next build` child -# (build-next-isolated.mjs → resolveNextBuildEnv spreads process.env). Build-only; -# the runtime heap is set separately on the runner stage (OMNIROUTE_MEMORY_MB). -# Override for hosts with more/less RAM: `--build-arg OMNIROUTE_BUILD_MEMORY_MB=6144`. +# pass needs more than V8's default ceiling (~2 GB) for a codebase this size; a +# memory-constrained Docker build otherwise dies with "FATAL ERROR: ... JavaScript +# heap out of memory" during the builder stage (#4076). Turbopack's compile is +# native (Rust) and less V8-heap-bound, but the prerender/export phase still runs +# on V8, so keep the ceiling. NODE_OPTIONS propagates to the spawned `next build` +# child (build-next-isolated.mjs → resolveNextBuildEnv spreads process.env). +# Build-only; the runtime heap is set separately on the runner stage +# (OMNIROUTE_MEMORY_MB). Override: `--build-arg OMNIROUTE_BUILD_MEMORY_MB=6144`. ARG OMNIROUTE_BUILD_MEMORY_MB=4096 ENV NODE_OPTIONS="--max-old-space-size=${OMNIROUTE_BUILD_MEMORY_MB}" COPY . ./ -RUN --mount=type=cache,target=/app/.build/next/cache \ +RUN --mount=type=cache,id=next-cache,target=/app/.build/next/cache \ mkdir -p /app/data && npm run build # ── Runner base ──────────────────────────────────────────────────────────── @@ -176,8 +184,8 @@ COPY --from=builder /app/node_modules/playwright ./node_modules/playwright # browsers land under /home/node which persists across image layers and is # accessible to the non-root runtime user. ENV PLAYWRIGHT_BROWSERS_PATH=/home/node/.cache/ms-playwright -RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ - --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \ +RUN --mount=type=cache,id=apt-cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,id=apt-lists,target=/var/lib/apt/lists,sharing=locked \ apt-get update \ && node node_modules/playwright/cli.js install chromium --with-deps \ && chown -R node:node /home/node/.cache \ @@ -193,15 +201,15 @@ FROM runner-base AS runner-cli USER root # Install system dependencies required by openclaw (git+ssh references). -RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ - --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \ +RUN --mount=type=cache,id=apt-cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,id=apt-lists,target=/var/lib/apt/lists,sharing=locked \ apt-get update \ && apt-get install -y --no-install-recommends git ca-certificates docker.io docker-compose \ && rm -rf /var/lib/apt/lists/* \ && git config --system url."https://github.com/".insteadOf "ssh://git@github.com/" # Install CLI tools globally. Separate layer from apt for better cache reuse. -RUN --mount=type=cache,target=/root/.npm \ +RUN --mount=type=cache,id=npm-cache,target=/root/.npm \ npm install -g --no-audit --no-fund @openai/codex @anthropic-ai/claude-code droid openclaw@latest USER node diff --git a/README.md b/README.md index 8f915b98c2..605901d698 100644 --- a/README.md +++ b/README.md @@ -275,7 +275,7 @@ Generous mode (<50% pool used) → idle shares are lent out Strict mode (≥50% pool used) → each key held to its fair share ``` -Enforced in the hot path **before** the request leaves OmniRoute, with per-(key, model) caps + session stickiness for prompt-cache integrity. 📖 [Quota Sharing Engine](docs/routing/QUOTA_SHARE.md) +Enforced in the hot path **before** the request leaves OmniRoute, with per-(key, model) caps + session stickiness for prompt-cache integrity (now with a per-combo / global disable toggle). 📖 [Quota Sharing Engine](docs/routing/QUOTA_SHARE.md) ## @@ -331,11 +331,11 @@ Result: 4 layers of fallback = zero downtime -> Recent highlights from **v3.8.20 → v3.8.44**. Full history in [`CHANGELOG.md`](CHANGELOG.md). +> Recent highlights from **v3.8.20 → v3.8.45**. Full history in [`CHANGELOG.md`](CHANGELOG.md). - **🗜️ Compression hardening** — a default-on **inflation guard** (discard the stacked result and send the verbatim original whenever compression would _grow_ the prompt), completed **Caveman rule packs** for German / French / Japanese (dedup + ultra) plus a new **Chinese (文言 / wényán) input pack** with zh-vs-ja auto-detection, and **RTK filters for Gradle & .NET (`dotnet`)** build output. → [Compression](docs/compression/COMPRESSION_ENGINES.md) - **💸 Honest flat-rate cost** — subscription / coding-plan providers (ChatGPT Web, grok-web, the Minimax / Kimi / GLM / Alibaba Coding plans, Xiaomi MiMo…) now read **$0** in cost analytics instead of an inflated per-token estimate, while budget / quota / routing keep estimating unchanged. → [API Reference](docs/reference/API_REFERENCE.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) +- **⚖️ 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 (now with a per-combo / global disable toggle), 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) - **🛰️ Remote mode** — drive a remote OmniRoute from any machine with scoped access tokens (`omniroute connect` / `omniroute contexts` / `omniroute tokens`), plus an `omniroute login antigravity` helper that runs Google "native/desktop" OAuth on your own machine and pastes a credential blob into a remote/VPS install (where the loopback redirect is unreachable). → [Remote Mode](docs/guides/REMOTE-MODE.md) - **🧭 Smarter auto-routing** — OpenRouter-style `auto/:` combos (e.g. `auto/coding:fast`, `auto/reasoning:pro`), a **Fusion** strategy (fan out to a panel of models in parallel, then synthesize via a judge), **task-aware routing** (best-fit connection per task type), per-request `X-Route-Model` override, live Arena-ELO + models.dev model intelligence, per-step account allowlists, provider-wildcard combo steps, nested combo-ref execution, sticky weighted selection, `web_search`-aware routing, and **per-request Auto-Combo controls** (`X-OmniRoute-Mode` mode-preset override + `X-OmniRoute-Budget` hard USD cost ceiling, scoped to a single request). → [Auto-Combo](docs/routing/AUTO-COMBO.md) @@ -345,8 +345,8 @@ Result: 4 layers of fallback = zero downtime - **🧠 Memory you control** — opt-in int8 vector quantization (Qdrant + sqlite-vec), opt-in **typed memory decay** (aged low-value memories fade on a per-type schedule), 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) - **🖼️ New endpoints** — `/v1/ocr` (Mistral OCR) and `/v1/audio/translations` (Whisper-style audio translation) round out the media API surface. → [API Reference](docs/reference/API_REFERENCE.md) -- **🌍 Deployment & ops** — reverse-proxy `basePath` deployment (`OMNIROUTE_BASE_PATH`, e.g. serving OmniRoute under `/omniroute/`), browser-language auto-detect on first visit, and per-API-key device/connection tracking (IP+UA fingerprint, masked, in-memory only). → [Environment](docs/reference/ENVIRONMENT.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), a first-class **Ollama** local-provider card, the **SenseNova** free Token Plan (chat + text-to-image), one-click account import from CLIProxyAPI (`~/.cli-proxy-api/`), **Claude Sonnet 5** wired end-to-end, a new provider wave (**Kenari**, **SumoPod**, **X5Lab**, **Charm Hyper**, **Nube.sh**, **b.ai**, **Qiniu**, **ModelScope**, **Augment/Auggie CLI**, **ClinePass**, NVIDIA NIM image generation), and Codex account import from a raw ChatGPT access token. → [Providers](docs/reference/PROVIDER_REFERENCE.md) +- **🌍 Deployment & ops** — reverse-proxy `basePath` deployment (`OMNIROUTE_BASE_PATH`, e.g. serving OmniRoute under `/omniroute/`), browser-language auto-detect on first visit, per-API-key device/connection tracking (IP+UA fingerprint, masked, in-memory only), root-less MITM cert trust for user-namespaced containers (`OMNIROUTE_NO_SUDO`), and server-side configured-only / available-only filters on the Free Provider Rankings page. → [Environment](docs/reference/ENVIRONMENT.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), a first-class **Ollama** local-provider card, the **SenseNova** free Token Plan (chat + text-to-image), one-click account import from CLIProxyAPI (`~/.cli-proxy-api/`), **Claude Sonnet 5** wired end-to-end, a new provider wave (**Kenari**, **SumoPod**, **X5Lab**, **Charm Hyper**, **Nube.sh**, **b.ai**, **Qiniu**, **ModelScope**, **Augment/Auggie CLI**, **ClinePass**, NVIDIA NIM image generation), Codex account import from a raw ChatGPT access token, the **Requesty** gateway (BYOK, ~200 free req/day), **Yuanbao (web)** as a cookie-session provider (DeepSeek V3/R1 + Hunyuan), the **Zed** hosted LLM aggregator (OAuth), **Claude 5 Sonnet** on the Claude Web provider, Kiro **adaptive-thinking reasoning** surfaced as `reasoning_content`, and **bulk API-key add for Cloudflare Workers AI**. → [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, a relay-backend selector (`OMNIROUTE_RELAY_BACKEND=ts|bifrost|auto`) so `/v1/relay` stays the stable surface while choosing the fastest backend internally, **Bifrost** (Go AI-gateway) and **Mux** (agent-orchestration daemon) promoted to first-class embedded/supervised services alongside 9Router/CLIProxyAPI, and **Webshare** added as a paid fourth source in the free-proxy provider framework. → [Embedded Services](docs/frameworks/EMBEDDED-SERVICES.md)
diff --git a/bin/omniroute.mjs b/bin/omniroute.mjs index 77f7418d65..e1ef7b0e9a 100755 --- a/bin/omniroute.mjs +++ b/bin/omniroute.mjs @@ -6,6 +6,7 @@ * Special bypasses (handled before Commander): * --mcp Start MCP server over stdio * reset-encrypted-columns Recovery tool for broken encrypted credentials + * reset-password Reset the admin/management password * * All other commands are routed through Commander (bin/cli/program.mjs). */ @@ -210,6 +211,15 @@ if (process.argv.includes("reset-encrypted-columns")) { process.exit(exitCode ?? 0); } +if (process.argv.includes("reset-password")) { + // bin/reset-password.mjs self-executes its `main()` on import and calls + // process.exit() on completion/error. Await a never-resolving promise so + // control never falls through to Commander (which would then reject + // `reset-password` as an unknown command). See #6261. + await import(pathToFileURL(join(ROOT, "bin", "reset-password.mjs")).href); + await new Promise(() => {}); +} + try { const { createProgram } = await import( pathToFileURL(join(ROOT, "bin", "cli", "program.mjs")).href diff --git a/bin/reset-password.mjs b/bin/reset-password.mjs index d21e2c5193..ab40282b0d 100755 --- a/bin/reset-password.mjs +++ b/bin/reset-password.mjs @@ -5,10 +5,15 @@ * * Usage: * node bin/reset-password.mjs - * npx omniroute reset-password + * omniroute reset-password + * + * Non-interactive / scripted usage (piped stdin, e.g. CI or Docker): + * printf 'NewPass123\nNewPass123\n' | omniroute reset-password + * printf 'NewPass123' | omniroute reset-password --password-stdin * * Resets the admin password for OmniRoute. - * Prompts for a new password and updates the database directly. + * Prompts for a new password (interactive TTY) or reads it from stdin + * (non-TTY) and updates the database directly. * * @module bin/reset-password */ @@ -21,19 +26,61 @@ import { readManagementPasswordState, resetManagementPassword } from "./cli/sqli const DATA_DIR = resolveDataDir(); const DB_PATH = resolveStoragePath(DATA_DIR); -const rl = createInterface({ - input: process.stdin, - output: process.stdout, -}); +const MIN_PASSWORD_LENGTH = 8; -function ask(question) { - return new Promise((resolve) => rl.question(question, resolve)); +/** Read the entire stdin stream as a UTF-8 string (used for non-TTY input). */ +function readAllStdin() { + return new Promise((resolve) => { + let data = ""; + process.stdin.setEncoding("utf8"); + process.stdin.on("data", (chunk) => { + data += chunk; + }); + process.stdin.on("end", () => resolve(data)); + process.stdin.on("error", () => resolve(data)); + // Resuming is implied by attaching a 'data' listener, but be explicit so a + // paused stream (some spawn setups) still flows to EOF. + process.stdin.resume(); + }); } -function exitWithError(message) { - console.error(message); - rl.close(); - process.exit(1); +/** + * Obtain the new password (and its confirmation). + * + * - `--password-stdin`: the ENTIRE stdin is the password, no confirmation. + * - non-TTY stdin (piped): read all of stdin once; first line is the password, + * second line — when present — is the confirmation, else the first line is + * reused (a single-line pipe means "no separate confirmation"). + * - interactive TTY: two sequential prompts (unchanged behavior). + * + * The non-TTY path exists because two sequential `rl.question` promises never + * settle under a piped EOF — the second read blocks forever, so the reset was + * silently never applied (#6258). + */ +async function collectPassword() { + if (process.argv.includes("--password-stdin")) { + const raw = await readAllStdin(); + const password = raw.replace(/[\r\n]+$/, ""); + return { password, confirm: password }; + } + + if (!process.stdin.isTTY) { + const raw = await readAllStdin(); + const lines = raw.split(/\r?\n/); + const password = lines[0] ?? ""; + const confirm = lines[1] ? lines[1] : password; + return { password, confirm }; + } + + const rl = createInterface({ input: process.stdin, output: process.stdout }); + try { + const ask = (question) => new Promise((resolve) => rl.question(question, resolve)); + const password = await ask("Enter new password (min 8 chars): "); + const confirm = await ask("Confirm new password: "); + return { password, confirm }; + } finally { + rl.close(); + } } console.log("\n🔑 OmniRoute — Password Reset\n"); @@ -54,27 +101,34 @@ async function main() { console.log("ℹ️ No password is currently set."); } - const password = await ask("Enter new password (min 8 chars): "); + const { password, confirm } = await collectPassword(); - if (!password || password.length < 8) { - exitWithError("\n❌ Password must be at least 8 characters.\n"); + if (!password || password.length < MIN_PASSWORD_LENGTH) { + console.error(`\n❌ Password must be at least ${MIN_PASSWORD_LENGTH} characters.\n`); + process.exit(1); } - const confirm = await ask("Confirm new password: "); - if (password !== confirm) { - exitWithError("\n❌ Passwords do not match.\n"); + console.error("\n❌ Passwords do not match.\n"); + process.exit(1); } await resetManagementPassword(password, DB_PATH); - rl.close(); console.log("\n✅ Password reset successfully!"); console.log(" Restart OmniRoute for changes to take effect.\n"); } -main().catch((err) => { - console.error(`\n❌ Error: ${err.message}\n`); - rl.close(); - process.exit(1); -}); +main() + .then(() => { + // Explicit exit(0) so a caller that imports this module (bin/omniroute.mjs + // routes `omniroute reset-password` here) terminates cleanly instead of + // hanging / exiting with code 13 on an unsettled wrapper await. On POSIX, + // console.log to a pipe is synchronous, so the success line is already + // flushed by the time we exit. + process.exit(0); + }) + .catch((err) => { + console.error(`\n❌ Error: ${err.message}\n`); + process.exit(1); + }); diff --git a/config/quality/complexity-baseline.json b/config/quality/complexity-baseline.json index 195757c2cc..1ca4a5ce4b 100644 --- a/config/quality/complexity-baseline.json +++ b/config/quality/complexity-baseline.json @@ -1,6 +1,6 @@ { "_comment": "Catraca de complexidade (check-complexity.mjs, ESLint core rules complexity>=15 e max-lines-per-function>80 sobre src+open-sse+electron+bin via eslint.complexity.config.mjs). Conta total de violacoes; so pode cair. --update ratcheta.", - "count": 2028, + "count": 2035, "_rebaseline_2026_07_04_v3844_release_close": "2026->2028 (+2). v3.8.44 release close (generate-release Phase 0/1): drift residual do fim do ciclo medido no tip pos-#6155 (merge burst final: #6155 cooling-panel + #6104 Kenari + #6139/#6128 provider-limits). Trust-but-verify: os 2 fixes de codigo do release-captain (model.ts alias boundary, auggie.ts stdin error handlers) adicionam 0 violacoes NOVAS — eslint.complexity direto nos 2 arquivos flagra apenas funcoes que ja estouravam o limite antes (runStreaming/start ja >80 linhas; resolveModelByProviderInference/getModelInfoCore pre-existentes de #5918), e resolveProviderAlias segue abaixo de 15. Logo o +2 e drift herdado do burst. Tighten via --update next cycle.", "_rebaseline_2026_07_03_v3844_ipfilter_release_green": "2015->2026 (+11). v3.8.44 cycle drift measured on release tip 32e4c906e during the #6131/#5975 release-green rebaseline. Inherited from the merge burst (Quality Ratchet does not run on PR->release fast-gates). route-edge-coverage +7 is my #5975 test comment; the rest is parallel-session drift. Tighten via --update next cycle.", "_rebaseline_2026_07_03_6007_sidecar_manifest": "2007->2015. PR #6007 (re-cut) adds sidecar/provider-manifest header wiring and the public manifest URL helper; the feature files themselves introduce 0 new complexity violations (check-complexity flags none in providerPluginManifestUrl.ts). The +8 vs the 2007 release baseline is inherited release/v3.8.44 drift absorbed at merge (check:complexity measures 2015 on the current release tip). Tighten via --update next cycle / at /generate-release Phase 0.", @@ -33,5 +33,6 @@ "_rebaseline_2026_06_13_v3825": "Re-baseline consciente: drift 1794->1800 (+6) do ciclo v3.8.24->v3.8.25 (features #3799-#3806). Mesma familia dos re-baselines anteriores — crescimento de feature legitima, nao regressao. Reducao fica como debt de refactor dedicado.", "_rebaseline_2026_06_10": "Re-baseline consciente: 1739 foi medido na branch das Fases 0-6 (base ~v3.8.17); a v3.8.18 publicada ja carrega 1746 (provado: o commit-base 5f2722bd6, anterior a qualquer commit do ciclo v3.8.19, mede 1746 — funcoes complexas dos reworks RequestLoggerV2/stream/combo). Mesma familia dos re-baselines de eslintWarnings/file-size. Reducao = Fase 6A (2026-06-16).", "_rebaseline_2026_06_13_6a11": "Re-baseline consciente Task 6A.11: escopo ampliado para src+open-sse+electron+bin (electron/bin contribuem 0 violacoes novas — todos os 4 arquivos .ts em bin/ estao abaixo dos thresholds). Drift 1746→1794 pre-existente de features mergeadas nos ciclos v3.8.22/v3.8.23 (nao causado por esta task). Congelado no valor real medido para destrancar o gate.", - "_rebaseline_2026_06_26_v3837_release": "1950->1963 (+13). 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." + "_rebaseline_2026_06_26_v3837_release": "1950->1963 (+13). 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.", + "_rebaseline_2026_07_06_v3845_release_close": "2028->2035 (+7). v3.8.45 release close (generate-release Phase 0): drift herdado do merge burst final do ciclo (#6216 streaming fixes, #6251/#6253 dashboard UX, #6292 zero-width, fixes do pre-flight ce897453 — todos test/config/workflow-neutros em complexidade nova, verificado pelo validador no tip 5ecca12aa5). Tighten via --update next cycle." } diff --git a/config/quality/eslint-suppressions.json b/config/quality/eslint-suppressions.json index e0f5b93a58..73470409b5 100644 --- a/config/quality/eslint-suppressions.json +++ b/config/quality/eslint-suppressions.json @@ -242,11 +242,6 @@ "count": 1 } }, - "src/app/(dashboard)/dashboard/settings/components/ComboDefaultsTab.tsx": { - "no-restricted-syntax": { - "count": 1 - } - }, "src/app/(dashboard)/dashboard/settings/components/ModelLockoutCard.tsx": { "react-hooks/exhaustive-deps": { "count": 1 @@ -1392,11 +1387,6 @@ "count": 1 } }, - "tests/unit/executor-kiro.test.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 1 - } - }, "tests/unit/executor-nlpcloud.test.ts": { "@typescript-eslint/no-explicit-any": { "count": 2 @@ -1567,11 +1557,6 @@ "count": 2 } }, - "tests/unit/messages-count-tokens-route.test.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 4 - } - }, "tests/unit/mimocode-executor.test.ts": { "@typescript-eslint/no-explicit-any": { "count": 58 @@ -1979,7 +1964,7 @@ }, "tests/unit/response-sanitizer.test.ts": { "@typescript-eslint/no-explicit-any": { - "count": 84 + "count": 83 } }, "tests/unit/responses-parse-once-4041.test.ts": { @@ -2002,11 +1987,6 @@ "count": 9 } }, - "tests/unit/save-call-log-persistence.test.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 7 - } - }, "tests/unit/schema-coercion.test.ts": { "@typescript-eslint/no-explicit-any": { "count": 12 diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index 925bc1eca6..39fbd592d2 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -185,7 +185,7 @@ "open-sse/translator/request/openai-to-kiro.ts": 890, "open-sse/translator/response/openai-responses.ts": 1092, "open-sse/utils/cursorAgentProtobuf.ts": 1521, - "open-sse/utils/stream.ts": 2727, + "open-sse/utils/stream.ts": 2792, "src/app/(dashboard)/dashboard/HomePageClient.tsx": 1385, "src/app/(dashboard)/dashboard/analytics/ComboHealthTab.tsx": 1028, "src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx": 3058, @@ -193,7 +193,7 @@ "src/app/(dashboard)/dashboard/cache/page.tsx": 845, "src/app/(dashboard)/dashboard/cli-code/components/CodexToolCard.tsx": 900, "src/app/(dashboard)/dashboard/cloud-agents/page.tsx": 922, - "src/app/(dashboard)/dashboard/combos/page.tsx": 4608, + "src/app/(dashboard)/dashboard/combos/page.tsx": 4655, "src/app/(dashboard)/dashboard/costs/CostOverviewTab.tsx": 1495, "src/app/(dashboard)/dashboard/costs/quota-share/components/PoolWizard.tsx": 1007, "src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx": 2612, @@ -201,21 +201,21 @@ "src/app/(dashboard)/dashboard/playground/components/tabs/ApiTab.tsx": 847, "src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx": 784, "src/app/(dashboard)/dashboard/providers/[id]/components/ConnectionRow.tsx": 942, - "src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx": 905, - "src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal.tsx": 1204, + "src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx": 915, + "src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal.tsx": 1231, "src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderConnections.ts": 954, "src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderModels.ts": 155, "src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderSettings.ts": 264, - "src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts": 1034, - "src/app/(dashboard)/dashboard/providers/components/onboarding/ProviderOnboardingWizard.tsx": 906, + "src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts": 1053, + "src/app/(dashboard)/dashboard/providers/components/onboarding/ProviderOnboardingWizard.tsx": 912, "src/app/(dashboard)/dashboard/providers/page.tsx": 1927, "src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx": 1201, "src/app/(dashboard)/dashboard/settings/components/AppearanceTab.tsx": 819, - "src/app/(dashboard)/dashboard/settings/components/ComboDefaultsTab.tsx": 851, + "src/app/(dashboard)/dashboard/settings/components/ComboDefaultsTab.tsx": 884, "src/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab.tsx": 974, "src/app/(dashboard)/dashboard/settings/components/MemorySkillsTab.tsx": 898, "src/app/(dashboard)/dashboard/settings/components/PricingTab.tsx": 1012, - "src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx": 1117, + "src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx": 1125, "src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx": 1183, "src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx": 1629, "src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx": 1924, @@ -247,7 +247,7 @@ "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.", "src/shared/components/OAuthModal.tsx": 993, - "src/shared/components/RequestLoggerV2.tsx": 1553, + "src/shared/components/RequestLoggerV2.tsx": 1629, "src/shared/components/analytics/charts.tsx": 1558, "src/shared/constants/cliTools.ts": 875, "src/shared/constants/pricing.ts": 1662, @@ -256,9 +256,9 @@ "src/shared/services/cliRuntime.ts": 1100, "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 791850: #5928 (test(security) Kimi Web URL host parse, CodeQL #689) grew the file +5 lines and merged into release/v3.8.44 WITHOUT rebaselining, leaving a fast-gates base-red that blocked every subsequent PR->release. Test growth is legitimate (a security regression test); maintainer absorbs the drift here. Frozen at 850.", "tests/unit/web-cookie-providers-new.test.ts": 890, - "tests/unit/response-sanitizer.test.ts": 906 + "tests/unit/response-sanitizer.test.ts": 1063 }, "_rebaseline_2026_06_09": "Re-baseline consciente pre-release v3.8.19: 9 arquivos cresceram durante o ciclo (features mergeadas: RequestLoggerV2 +281 request-logger rework, stream +101, combo +73, chatCore +45, catalog +32 fable-5/catalog-flag, callLogs +4, accountFallback +2, usageHistory novo 840) + core.ts +7 (fix resetAllDbModuleState, PR 3536). A catraca segue valendo destes valores — proximo crescimento falha. Decisao: encolher (esp. RequestLoggerV2/chatCore) e a issue #3501 ficam para o ciclo seguinte.", "_rebaseline_2026_06_11_phase1f": "Phase 1f (#3501): ProviderDetailPageClient.tsx 4948→4062 (-886 LOC); 3 novos hooks extraídos. useProviderConnections.ts=954 acima do cap=800 — justificado: extração direta do god-component (zero lógica nova), própria redução do cliente supera o custo. useProviderSettings.ts=263 e useProviderModels.ts=154 já abaixo do cap.", @@ -376,5 +376,6 @@ "_rebaseline_2026_07_05_6154_copilot_catalog_helpers": "PR #6154 own growth: src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts 1021->1034 (+13 = GitHub Copilot catalog refresh — model-section helper wiring for the refreshed passthrough/compatible model lists). Cohesive UI-helper growth alongside the registry/modelSpecs catalog refresh; not extractable. Covered by the PR's provider-registry-github-copilot-* unit tests. Fast-path PR->release skips check:file-size, so this bump lands with the PR (contributor backryun).", "_rebaseline_2026_07_05_6213_kiro_thinking_filesize": "PR #6213 own growth (kiro adaptive-thinking -> reasoning_content, +384): open-sse/translator/request/openai-to-kiro.ts 853->890 (+37 = additionalModelRequestFields builder for adaptive thinking: output_config.effort + thinking:{type:adaptive} + max_tokens, only when the request asked for thinking) and tests/unit/translator-openai-to-kiro.test.ts 1093->1234 (+141 = adaptive-thinking request/frame regression cases). The fast-path PR->release does NOT gate check:file-size on the merge, so this cohesive feature growth accumulated on the release tip (see the 2026-07-02 #5798 note for the same pattern). Superseded by the release captain's rebaseline-at-release.", "_rebaseline_2026_07_05_6235_doubao_dola": "PR #6235 own growth: tests/unit/web-cookie-providers-new.test.ts 850->890 (+40 = doubao-web -> Dola global provider switch regression cases: new host/cookie-domain/token-source assertions for www.dola.com). Cohesive test growth alongside the provider switch; contributor backryun. Fast-path PR->release skips check:file-size, so this bump lands with the PR.", - "_rebaseline_2026_07_05_6118_zed_hosted": "PR #6118 own growth: src/shared/components/OAuthModal.tsx 989->993 (+4 = Zed hosted native-app sign-in modal branch). Cohesive UI growth for the new zed-hosted OAuth provider; not extractable. Fast-path PR->release skips check:file-size." + "_rebaseline_2026_07_06_v3845_release_close": "Release v3.8.45 cycle-close rebaseline (captain, sess ce897453): 13 files grown by the cycle's merged fix/feature PRs (#6216 streaming fixes + request-logger UI grew RequestLoggerV2/chat/chatHelpers/auth/stream/response-sanitizer.test; #6251/#6253 dashboard UX grew combos page/modals/wizard/ComboDefaultsTab/ProxyRegistryManager/providerPageHelpers). Growth is legitimate merged-feature code, absorbed at release per Phase 0 drift policy; all remain frozen (cannot grow further).", + "_rebaseline_2026_07_06_6118_zed_oauthmodal": "PR #6118 own growth: OAuthModal.tsx 989->993 (+4 = Zed hosted native-app sign-in modal branch). Cohesive UI growth for the zed-hosted OAuth provider; not extractable. The prior 6118 comment set the note but left the frozen value at 989." } diff --git a/config/quality/quality-baseline.json b/config/quality/quality-baseline.json index 3b49d0d0b4..b664cb3bdd 100644 --- a/config/quality/quality-baseline.json +++ b/config/quality/quality-baseline.json @@ -120,7 +120,7 @@ "_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": 867, + "value": 877, "_rebaseline_2026_07_03_v3844_ipfilter_release_green": "861->867 (+6). v3.8.44 cycle drift measured on release tip 32e4c906e during the #6131/#5975 release-green rebaseline. Inherited from the merge burst (Quality Ratchet does not run on PR->release fast-gates). route-edge-coverage +7 is my #5975 test comment; the rest is parallel-session drift. Tighten via --update next cycle.", "_rebaseline_2026_07_03_v3844_review_prs_fix_batch": "860->861 (+1). Inherited v3.8.44 cycle drift surfaced by the release-green pre-flight during the /review-prs fix-batch round; check:cognitive-complexity measures 861 on the release tip 72ee80649. Negligible +1 from the round's / parallel-session merge burst (cognitive-complexity does NOT run on PR->release fast-gates). Structural shrink tracked in #3501. Tighten via --update next cycle.", "_rebaseline_2026_07_02_v3844_post_5939": "859->860 (+1). Same inherited post-3a3d618fe release drift as the complexity note of this date; this PR touches one CLI line (nullish default, no new function over the threshold) and one test file (not scanned). Tighten via --update next cycle.", @@ -135,7 +135,8 @@ "_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.", "_rebaseline_2026_06_22_v3833_release": "793→797 (+4) — pre-existing cycle drift on origin/release/v3.8.33.", "_rebaseline_2026_06_26_v3837_release": "816->826. 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.", - "_rebaseline_2026_06_26_v3838_release": "826->833. v3.8.38 release base measures 833 locally on origin/release/v3.8.38 (800b04ad6) while the committed baseline still says 826. This PR measures the same 833 after refactoring jsonToSse helpers back under the sonarjs/cognitive-complexity threshold, so it does not add a net cognitive-complexity violation. The baseline bump records inherited release-base drift only; structural shrink remains tracked by the existing chatCore decomposition work." + "_rebaseline_2026_06_26_v3838_release": "826->833. v3.8.38 release base measures 833 locally on origin/release/v3.8.38 (800b04ad6) while the committed baseline still says 826. This PR measures the same 833 after refactoring jsonToSse helpers back under the sonarjs/cognitive-complexity threshold, so it does not add a net cognitive-complexity violation. The baseline bump records inherited release-base drift only; structural shrink remains tracked by the existing chatCore decomposition work.", + "_rebaseline_2026_07_06_v3845_release_close": "867->877 (+10). v3.8.45 cycle drift measured by check:release-green (hermetic) on release tip 5ecca12aa5 during the /generate-release Phase 0 pre-flight. Inherited from the cycle's merge burst (cognitive-complexity does not run on PR->release fast-gates); the captain's pre-flight fixes are gate/test/workflow changes (complexity-neutral). Tighten via --update next cycle." }, "typeCoveragePct": { "value": 92.17, diff --git a/config/quality/test-masking-allowlist.json b/config/quality/test-masking-allowlist.json index 2529dd5267..d548c4a40c 100644 --- a/config/quality/test-masking-allowlist.json +++ b/config/quality/test-masking-allowlist.json @@ -21,6 +21,18 @@ "open-sse/services/combo/__tests__/targetExhaustion.test.ts": { "replacement": "tests/unit/combo/combo-target-exhaustion.test.ts", "reason": "v3.8.44 #5976: os testes de exaustão eram flake-prone (delays Math.random, timeouts 30s, >3min no CI) e foram REESCRITOS como unit determinístico com MAIS cobertura (21 casos/52 asserts vs 13 casos/37 asserts). Documentado no commit 5fe225850. Revisão humana: apresentado ao operador no STOP #1 do release v3.8.44." + }, + "src/shared/components/AutoRoutingBanner.test.tsx": { + "replacement": "tests/unit/home-no-autorouting-banner.test.ts", + "reason": "v3.8.45 #6164: fix(dashboard) remove the always-on Auto-Routing banner — o COMPONENTE foi deletado junto com o teste (feature removida pelo mantenedor, não mascaramento). O replacement guarda o novo contrato: a home NÃO renderiza o banner e o componente permanece deletado." + }, + "tests/unit/free-provider-rankings-configured-filter.test.ts": { + "replacement": "tests/unit/freeProviderRankings-filters.test.ts", + "reason": "v3.8.45 #6251 supersede #6245: a página Free Provider Rankings migrou do toggle client-side 'Configured Only' (#6245, configuredProviderIds no cliente) para filtros server-side configuredOnly/availableOnly (#6251). O teste antigo pinava a implementação removida (7 asserts quebrados contra código que não existe); o replacement cobre o contrato novo com 11 casos (server-side, lib helper). Verificado legítimo — supersessão documentada no CHANGELOG do #6251." } - } + }, + "tests/unit/catalog-updates-v3x.test.ts": "v3.8.45 #6248: fix(providers) remove deprecated MiMo V2 entries — os 5 asserts removidos pinavam specs de modelos mimo-v2-* que deixaram de existir no catálogo (54→49). Asserts seguem a remoção dos modelos, não enfraquecimento. Verificado legítimo. Prune após v3.8.45 mergear para main.", + "tests/unit/xiaomi-mimo-provider.test.ts": "v3.8.45 #6248: fix(providers) remove deprecated MiMo V2 entries — 2 asserts sobre mimo-v2-pro/omni/flash removidos junto com os modelos (21→19). Verificado legítimo, não mascaramento. Prune após v3.8.45 mergear para main.", + "tests/unit/provider-models-route.test.ts": "v3.8.45 #6170: fix(providers) correct Kiro model catalog to real upstream ids — 3 assert.ok positivos de ids fabricados (claude-opus-4.7/sonnet-4.6) substituídos por 1 assert positivo de conjunto + 1 assert NEGATIVO garantindo que os ids fabricados sumiram (310→309). Asserts migrados ao catálogo real, não enfraquecidos. Verificado legítimo. Prune após v3.8.45 mergear para main.", + "tests/unit/copilot-gemini-claude-route-no-responses.test.ts": "v3.8.45 #6154: fix(providers) refresh GitHub Copilot catalog — 2 asserts combinados (registry-exists + par claude/gemini) reescritos como loop per-model com assert.ok individual por id do catálogo novo (7→6). Asserts reestruturados ao catálogo atualizado, não enfraquecidos. Verificado legítimo. Prune após v3.8.45 mergear para main." } diff --git a/docs/architecture/ARCHITECTURE.md b/docs/architecture/ARCHITECTURE.md index e91387e535..0dc4bbfc4d 100644 --- a/docs/architecture/ARCHITECTURE.md +++ b/docs/architecture/ARCHITECTURE.md @@ -17,7 +17,7 @@ It provides a single OpenAI-compatible endpoint (`/v1/*`) and routes traffic acr Core capabilities: -- OpenAI-compatible API surface for CLI/tools (237 providers, 68 executors) +- OpenAI-compatible API surface for CLI/tools (237 providers, 73 executors) - Request/response translation across provider formats - Model combo fallback (multi-model sequence) - Structured combo steps (`provider + model + connection`) with runtime ordering by `compositeTiers` diff --git a/docs/architecture/CODEBASE_DOCUMENTATION.md b/docs/architecture/CODEBASE_DOCUMENTATION.md index 599c5dc58b..0bd52c6599 100644 --- a/docs/architecture/CODEBASE_DOCUMENTATION.md +++ b/docs/architecture/CODEBASE_DOCUMENTATION.md @@ -482,7 +482,7 @@ open-sse/ ### 4.2 `open-sse/executors/` -68 provider executors, each extending `BaseExecutor` (`base.ts`): +73 provider executors, each extending `BaseExecutor` (`base.ts`): `antigravity`, `azure-openai`, `blackbox-web`, `chatgpt-web`, `cliproxyapi`, `cloudflare-ai`, `codex`, `commandCode`, `cursor`, `default`, `devin-cli`, @@ -522,21 +522,21 @@ Hub-and-spoke translation (OpenAI is the hub). Highlights (full list under `open-sse/services/`): -| Concern | Files | -| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| Combo routing | `combo.ts` (17 strategies), `comboConfig.ts`, `comboMetrics.ts`, `comboManifestMetrics.ts`, `comboAgentMiddleware.ts` | -| Auto Combo engine | `autoCombo/` — `engine.ts`, `scoring.ts`, `taskFitness.ts`, `virtualFactory.ts`, `modePacks.ts`, `autoPrefix.ts`, `persistence.ts`, `providerDiversity.ts`, `providerRegistryAccessor.ts`, `routerStrategy.ts`, `selfHealing.ts`, `index.ts` | -| Resilience | `accountFallback.ts` (cooldown + lockout), `errorClassifier.ts`, `emergencyFallback.ts`, `rateLimitManager.ts`, `rateLimitSemaphore.ts`, `accountSemaphore.ts`, `accountSelector.ts` | -| Quotas | `quotaMonitor.ts`, `quotaPreflight.ts`, `bailianQuotaFetcher.ts`, `codexQuotaFetcher.ts`, `deepseekQuotaFetcher.ts`, `crofUsageFetcher.ts`, `antigravityCredits.ts` | -| Caching | `reasoningCache.ts`, `searchCache.ts`, `signatureCache.ts`, `requestDedup.ts` | -| Routing intelligence | `intentClassifier.ts`, `taskAwareRouter.ts`, `backgroundTaskDetector.ts`, `volumeDetector.ts`, `wildcardRouter.ts`, `workflowFSM.ts`, `specificityDetector.ts`, `specificityRules.ts`, `specificityTypes.ts` | -| Model handling | `modelCapabilities.ts`, `modelDeprecation.ts`, `modelFamilyFallback.ts`, `modelStrip.ts`, `model.ts`, `provider.ts`, `providerRequestDefaults.ts`, `providerCostData.ts`, `payloadRules.ts` | -| Compression | `compression/` — full compression engine wiring | -| Token + session | `tokenRefresh.ts`, `sessionManager.ts`, `apiKeyRotator.ts`, `contextManager.ts`, `contextHandoff.ts`, `systemPrompt.ts`, `roleNormalizer.ts`, `responsesInputSanitizer.ts`, `toolSchemaSanitizer.ts`, `toolLimitDetector.ts`, `thinkingBudget.ts` | -| Tier / manifest | `tierResolver.ts`, `tierConfig.ts`, `tierDefaults.json`, `tierTypes.ts`, `manifestAdapter.ts` | -| IP / network | `ipFilter.ts`, `webSearchFallback.ts` | -| Batches | `batchProcessor.ts` | -| Usage | `usage.ts` | +| Concern | Files | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Combo routing | `combo.ts` (17 strategies), `comboConfig.ts`, `comboMetrics.ts`, `comboManifestMetrics.ts`, `comboAgentMiddleware.ts` | +| Auto Combo engine | `autoCombo/` — `engine.ts`, `scoring.ts`, `taskFitness.ts`, `virtualFactory.ts`, `modePacks.ts`, `autoPrefix.ts`, `persistence.ts`, `providerDiversity.ts`, `providerRegistryAccessor.ts`, `routerStrategy.ts`, `selfHealing.ts`, `index.ts` | +| Resilience | `accountFallback.ts` (cooldown + lockout), `errorClassifier.ts`, `emergencyFallback.ts`, `rateLimitManager.ts`, `rateLimitSemaphore.ts`, `accountSemaphore.ts`, `accountSelector.ts` | +| Quotas | `quotaMonitor.ts`, `quotaPreflight.ts`, `bailianQuotaFetcher.ts`, `codexQuotaFetcher.ts`, `deepseekQuotaFetcher.ts`, `crofUsageFetcher.ts`, `antigravityCredits.ts` | +| Caching | `reasoningCache.ts`, `searchCache.ts`, `signatureCache.ts`, `requestDedup.ts` | +| Routing intelligence | `intentClassifier.ts`, `taskAwareRouter.ts`, `backgroundTaskDetector.ts`, `volumeDetector.ts`, `wildcardRouter.ts`, `workflowFSM.ts`, `specificityDetector.ts`, `specificityRules.ts`, `specificityTypes.ts` | +| Model handling | `modelCapabilities.ts`, `modelDeprecation.ts`, `modelFamilyFallback.ts`, `modelStrip.ts`, `model.ts`, `provider.ts`, `providerRequestDefaults.ts`, `providerCostData.ts`, `payloadRules.ts` | +| Compression | `compression/` — full compression engine wiring | +| Token + session | `tokenRefresh.ts`, `sessionManager.ts`, `apiKeyRotator.ts`, `contextManager.ts`, `contextHandoff.ts`, `systemPrompt.ts`, `roleNormalizer.ts`, `responsesInputSanitizer.ts`, `toolSchemaSanitizer.ts`, `toolLimitDetector.ts`, `thinkingBudget.ts` | +| Tier / manifest | `tierResolver.ts`, `tierConfig.ts`, `tierDefaults.json`, `tierTypes.ts`, `manifestAdapter.ts` | +| IP / network | `ipFilter.ts`, `webSearchFallback.ts` | +| Batches | `batchProcessor.ts` | +| Usage | `usage.ts` | ### 4.6 `open-sse/mcp-server/` diff --git a/docs/architecture/REPOSITORY_MAP.md b/docs/architecture/REPOSITORY_MAP.md index 05b4488e14..2b1c5676d0 100644 --- a/docs/architecture/REPOSITORY_MAP.md +++ b/docs/architecture/REPOSITORY_MAP.md @@ -182,7 +182,7 @@ src/ | `compliance/` | Audit log + provider audit — see `docs/security/COMPLIANCE.md` | | `compression/` | Compression engine glue (engines live in `open-sse/services/compression/`) | | `config/` | Runtime config helpers | -| `db/` | 45+ domain DB modules + 55 migrations (always go through here for SQLite) | +| `db/` | 95+ domain DB modules + 110+ migrations (always go through here for SQLite) | | `quota/` | Quota Sharing Engine: `dimensions.ts` (types/Zod), `types.ts` (QuotaStore interface), `sqliteQuotaStore.ts`, `redisQuotaStore.ts`, `storeFactory.ts`, `fairShare.ts`, `burnRate.ts`, `planResolver.ts`, `planRegistry.ts`, `saturationSignals.ts`, `enforce.ts`, `spendRecorder.ts` — see `docs/routing/QUOTA_SHARE.md` | | `display/` | UI formatting helpers (cost, latency, etc.) | | `embeddings/` | Embeddings service helpers | diff --git a/docs/diagrams/db-schema-overview.mmd b/docs/diagrams/db-schema-overview.mmd index debf71f3b1..b5554eccf8 100644 --- a/docs/diagrams/db-schema-overview.mmd +++ b/docs/diagrams/db-schema-overview.mmd @@ -1,5 +1,5 @@ %% Database schema overview (selected core tables) -%% Reflects: src/lib/db/* (45+ modules, 55 migrations) +%% Reflects: src/lib/db/* (95+ modules, 110+ migrations) %% v3.8.0 erDiagram api_keys ||--o{ api_key_usage : tracks diff --git a/docs/i18n/ar/CHANGELOG.md b/docs/i18n/ar/CHANGELOG.md index fd67be1364..bc446f0403 100644 --- a/docs/i18n/ar/CHANGELOG.md +++ b/docs/i18n/ar/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/ar/llm.txt b/docs/i18n/ar/llm.txt index 4fa9b21284..775cd2b66e 100644 --- a/docs/i18n/ar/llm.txt +++ b/docs/i18n/ar/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/az/CHANGELOG.md b/docs/i18n/az/CHANGELOG.md index 50afbca45d..eaef8ac203 100644 --- a/docs/i18n/az/CHANGELOG.md +++ b/docs/i18n/az/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/az/llm.txt b/docs/i18n/az/llm.txt index 3950dcb984..45c98fe7db 100644 --- a/docs/i18n/az/llm.txt +++ b/docs/i18n/az/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/bg/CHANGELOG.md b/docs/i18n/bg/CHANGELOG.md index 50afbca45d..eaef8ac203 100644 --- a/docs/i18n/bg/CHANGELOG.md +++ b/docs/i18n/bg/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/bg/llm.txt b/docs/i18n/bg/llm.txt index 3950dcb984..45c98fe7db 100644 --- a/docs/i18n/bg/llm.txt +++ b/docs/i18n/bg/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/bn/CHANGELOG.md b/docs/i18n/bn/CHANGELOG.md index bcc49a1d43..33ecbe9133 100644 --- a/docs/i18n/bn/CHANGELOG.md +++ b/docs/i18n/bn/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/bn/llm.txt b/docs/i18n/bn/llm.txt index 40552a5a11..fdb0178a80 100644 --- a/docs/i18n/bn/llm.txt +++ b/docs/i18n/bn/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/cs/CHANGELOG.md b/docs/i18n/cs/CHANGELOG.md index ebff0c0cef..a9a360b497 100644 --- a/docs/i18n/cs/CHANGELOG.md +++ b/docs/i18n/cs/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/cs/llm.txt b/docs/i18n/cs/llm.txt index ce6de2f065..baf7b823d8 100644 --- a/docs/i18n/cs/llm.txt +++ b/docs/i18n/cs/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/da/CHANGELOG.md b/docs/i18n/da/CHANGELOG.md index 2e2294a472..a4596b5ad9 100644 --- a/docs/i18n/da/CHANGELOG.md +++ b/docs/i18n/da/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/da/llm.txt b/docs/i18n/da/llm.txt index d9b36886d6..819865a7ed 100644 --- a/docs/i18n/da/llm.txt +++ b/docs/i18n/da/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/de/CHANGELOG.md b/docs/i18n/de/CHANGELOG.md index 95ab5e1a0e..4fb4fcce3b 100644 --- a/docs/i18n/de/CHANGELOG.md +++ b/docs/i18n/de/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/de/llm.txt b/docs/i18n/de/llm.txt index 80f08179c9..fd8b2bb12c 100644 --- a/docs/i18n/de/llm.txt +++ b/docs/i18n/de/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/es/CHANGELOG.md b/docs/i18n/es/CHANGELOG.md index fcd34f3c34..b67286d967 100644 --- a/docs/i18n/es/CHANGELOG.md +++ b/docs/i18n/es/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/es/llm.txt b/docs/i18n/es/llm.txt index a1cb33aff2..dc3ca451f0 100644 --- a/docs/i18n/es/llm.txt +++ b/docs/i18n/es/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/fa/CHANGELOG.md b/docs/i18n/fa/CHANGELOG.md index d609d64c99..67bce1e461 100644 --- a/docs/i18n/fa/CHANGELOG.md +++ b/docs/i18n/fa/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/fa/llm.txt b/docs/i18n/fa/llm.txt index 6ba4136624..6848cd28f9 100644 --- a/docs/i18n/fa/llm.txt +++ b/docs/i18n/fa/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/fi/CHANGELOG.md b/docs/i18n/fi/CHANGELOG.md index 041b8b4b96..4158bbfdcd 100644 --- a/docs/i18n/fi/CHANGELOG.md +++ b/docs/i18n/fi/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/fi/llm.txt b/docs/i18n/fi/llm.txt index 7c5e76fec8..96224f8a4d 100644 --- a/docs/i18n/fi/llm.txt +++ b/docs/i18n/fi/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/fr/CHANGELOG.md b/docs/i18n/fr/CHANGELOG.md index da0d19876f..b83e130236 100644 --- a/docs/i18n/fr/CHANGELOG.md +++ b/docs/i18n/fr/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/fr/llm.txt b/docs/i18n/fr/llm.txt index ead67c7773..7de5c69aac 100644 --- a/docs/i18n/fr/llm.txt +++ b/docs/i18n/fr/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/gu/CHANGELOG.md b/docs/i18n/gu/CHANGELOG.md index 4fec859bc5..4ecfdd8279 100644 --- a/docs/i18n/gu/CHANGELOG.md +++ b/docs/i18n/gu/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/gu/llm.txt b/docs/i18n/gu/llm.txt index 65877bf751..3123e0b0aa 100644 --- a/docs/i18n/gu/llm.txt +++ b/docs/i18n/gu/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/he/CHANGELOG.md b/docs/i18n/he/CHANGELOG.md index 8af769ce6e..66fd82675b 100644 --- a/docs/i18n/he/CHANGELOG.md +++ b/docs/i18n/he/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/he/llm.txt b/docs/i18n/he/llm.txt index 8f0e22ab34..f73b2bb1cc 100644 --- a/docs/i18n/he/llm.txt +++ b/docs/i18n/he/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/hi/CHANGELOG.md b/docs/i18n/hi/CHANGELOG.md index cb32d371d6..85840d7fd4 100644 --- a/docs/i18n/hi/CHANGELOG.md +++ b/docs/i18n/hi/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/hi/llm.txt b/docs/i18n/hi/llm.txt index ac3b0774ea..64c9880884 100644 --- a/docs/i18n/hi/llm.txt +++ b/docs/i18n/hi/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/hu/CHANGELOG.md b/docs/i18n/hu/CHANGELOG.md index a8f46ec69d..d900de2702 100644 --- a/docs/i18n/hu/CHANGELOG.md +++ b/docs/i18n/hu/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/hu/llm.txt b/docs/i18n/hu/llm.txt index 2f6ad261a3..9f574945c0 100644 --- a/docs/i18n/hu/llm.txt +++ b/docs/i18n/hu/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/id/CHANGELOG.md b/docs/i18n/id/CHANGELOG.md index 7f683f4e25..fed9ca94b7 100644 --- a/docs/i18n/id/CHANGELOG.md +++ b/docs/i18n/id/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/id/llm.txt b/docs/i18n/id/llm.txt index 4a31546f56..e05008dedb 100644 --- a/docs/i18n/id/llm.txt +++ b/docs/i18n/id/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/in/CHANGELOG.md b/docs/i18n/in/CHANGELOG.md index 7f9cace70b..48f1e41782 100644 --- a/docs/i18n/in/CHANGELOG.md +++ b/docs/i18n/in/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/in/llm.txt b/docs/i18n/in/llm.txt index 6eee7c1d9a..6e91c869fd 100644 --- a/docs/i18n/in/llm.txt +++ b/docs/i18n/in/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/it/CHANGELOG.md b/docs/i18n/it/CHANGELOG.md index 6603d6f599..30e5d2fe9e 100644 --- a/docs/i18n/it/CHANGELOG.md +++ b/docs/i18n/it/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/it/llm.txt b/docs/i18n/it/llm.txt index 5ab3252731..6db7b5f06a 100644 --- a/docs/i18n/it/llm.txt +++ b/docs/i18n/it/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/ja/CHANGELOG.md b/docs/i18n/ja/CHANGELOG.md index 6598e1772d..6a8a8474ed 100644 --- a/docs/i18n/ja/CHANGELOG.md +++ b/docs/i18n/ja/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/ja/llm.txt b/docs/i18n/ja/llm.txt index 6ce95eddb2..ab529087fe 100644 --- a/docs/i18n/ja/llm.txt +++ b/docs/i18n/ja/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/ko/CHANGELOG.md b/docs/i18n/ko/CHANGELOG.md index ae7041112f..3fa91d11f0 100644 --- a/docs/i18n/ko/CHANGELOG.md +++ b/docs/i18n/ko/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/ko/llm.txt b/docs/i18n/ko/llm.txt index 8a8b338b57..8c0aeb75c6 100644 --- a/docs/i18n/ko/llm.txt +++ b/docs/i18n/ko/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/mr/CHANGELOG.md b/docs/i18n/mr/CHANGELOG.md index 9b4865b8c1..2c46ea4788 100644 --- a/docs/i18n/mr/CHANGELOG.md +++ b/docs/i18n/mr/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/mr/llm.txt b/docs/i18n/mr/llm.txt index 7536b91276..cde497ddc3 100644 --- a/docs/i18n/mr/llm.txt +++ b/docs/i18n/mr/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/ms/CHANGELOG.md b/docs/i18n/ms/CHANGELOG.md index 17eab69b7e..ce25911cc8 100644 --- a/docs/i18n/ms/CHANGELOG.md +++ b/docs/i18n/ms/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/ms/llm.txt b/docs/i18n/ms/llm.txt index c33b36f427..daf937c1f3 100644 --- a/docs/i18n/ms/llm.txt +++ b/docs/i18n/ms/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/nl/CHANGELOG.md b/docs/i18n/nl/CHANGELOG.md index e97cf6d61d..937ab026ee 100644 --- a/docs/i18n/nl/CHANGELOG.md +++ b/docs/i18n/nl/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/nl/llm.txt b/docs/i18n/nl/llm.txt index dcad1b15b1..38c2c89661 100644 --- a/docs/i18n/nl/llm.txt +++ b/docs/i18n/nl/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/no/CHANGELOG.md b/docs/i18n/no/CHANGELOG.md index 39c063287b..f015e1b488 100644 --- a/docs/i18n/no/CHANGELOG.md +++ b/docs/i18n/no/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/no/llm.txt b/docs/i18n/no/llm.txt index 79b1d8c5dd..1d19c59c96 100644 --- a/docs/i18n/no/llm.txt +++ b/docs/i18n/no/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/phi/CHANGELOG.md b/docs/i18n/phi/CHANGELOG.md index f15b08d683..869a84d9d1 100644 --- a/docs/i18n/phi/CHANGELOG.md +++ b/docs/i18n/phi/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/phi/llm.txt b/docs/i18n/phi/llm.txt index dbb53b1aca..783034fdfb 100644 --- a/docs/i18n/phi/llm.txt +++ b/docs/i18n/phi/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/pl/CHANGELOG.md b/docs/i18n/pl/CHANGELOG.md index 52447603f4..8d1a6c5ef3 100644 --- a/docs/i18n/pl/CHANGELOG.md +++ b/docs/i18n/pl/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/pl/llm.txt b/docs/i18n/pl/llm.txt index 5504d9e4d4..1072ad9f24 100644 --- a/docs/i18n/pl/llm.txt +++ b/docs/i18n/pl/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/pt-BR/CHANGELOG.md b/docs/i18n/pt-BR/CHANGELOG.md index f5a408df0b..903f753d36 100644 --- a/docs/i18n/pt-BR/CHANGELOG.md +++ b/docs/i18n/pt-BR/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/pt-BR/llm.txt b/docs/i18n/pt-BR/llm.txt index bedcfae6e2..c117a31086 100644 --- a/docs/i18n/pt-BR/llm.txt +++ b/docs/i18n/pt-BR/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/pt/CHANGELOG.md b/docs/i18n/pt/CHANGELOG.md index dd53f77c5f..3b4d45db03 100644 --- a/docs/i18n/pt/CHANGELOG.md +++ b/docs/i18n/pt/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/pt/llm.txt b/docs/i18n/pt/llm.txt index c9cdf06e64..0624b72f3b 100644 --- a/docs/i18n/pt/llm.txt +++ b/docs/i18n/pt/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/ro/CHANGELOG.md b/docs/i18n/ro/CHANGELOG.md index eda5fe9cfd..aa90eca149 100644 --- a/docs/i18n/ro/CHANGELOG.md +++ b/docs/i18n/ro/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/ro/llm.txt b/docs/i18n/ro/llm.txt index d34991f191..2810dae83d 100644 --- a/docs/i18n/ro/llm.txt +++ b/docs/i18n/ro/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/ru/CHANGELOG.md b/docs/i18n/ru/CHANGELOG.md index d6ddafdf59..e1d6d63079 100644 --- a/docs/i18n/ru/CHANGELOG.md +++ b/docs/i18n/ru/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/ru/llm.txt b/docs/i18n/ru/llm.txt index 6065228f78..923ed9e785 100644 --- a/docs/i18n/ru/llm.txt +++ b/docs/i18n/ru/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/sk/CHANGELOG.md b/docs/i18n/sk/CHANGELOG.md index 4033c72ffa..2551195f67 100644 --- a/docs/i18n/sk/CHANGELOG.md +++ b/docs/i18n/sk/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/sk/llm.txt b/docs/i18n/sk/llm.txt index 35d7392e29..bf2646ae46 100644 --- a/docs/i18n/sk/llm.txt +++ b/docs/i18n/sk/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/sv/CHANGELOG.md b/docs/i18n/sv/CHANGELOG.md index 107849ddf5..d64050bd4d 100644 --- a/docs/i18n/sv/CHANGELOG.md +++ b/docs/i18n/sv/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/sv/llm.txt b/docs/i18n/sv/llm.txt index 885a484dfd..9415c9010d 100644 --- a/docs/i18n/sv/llm.txt +++ b/docs/i18n/sv/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/sw/CHANGELOG.md b/docs/i18n/sw/CHANGELOG.md index b72838a986..942bf1b22f 100644 --- a/docs/i18n/sw/CHANGELOG.md +++ b/docs/i18n/sw/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/sw/llm.txt b/docs/i18n/sw/llm.txt index 106571d579..f9ebdc2a3d 100644 --- a/docs/i18n/sw/llm.txt +++ b/docs/i18n/sw/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/ta/CHANGELOG.md b/docs/i18n/ta/CHANGELOG.md index 02dcf8fae1..07d637e24d 100644 --- a/docs/i18n/ta/CHANGELOG.md +++ b/docs/i18n/ta/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/ta/llm.txt b/docs/i18n/ta/llm.txt index ef2f4fc981..91f2022328 100644 --- a/docs/i18n/ta/llm.txt +++ b/docs/i18n/ta/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/te/CHANGELOG.md b/docs/i18n/te/CHANGELOG.md index fefb40e53c..5773662731 100644 --- a/docs/i18n/te/CHANGELOG.md +++ b/docs/i18n/te/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/te/llm.txt b/docs/i18n/te/llm.txt index 96d1bab868..1de33d1956 100644 --- a/docs/i18n/te/llm.txt +++ b/docs/i18n/te/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/th/CHANGELOG.md b/docs/i18n/th/CHANGELOG.md index be4f09992e..44349c20c1 100644 --- a/docs/i18n/th/CHANGELOG.md +++ b/docs/i18n/th/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/th/llm.txt b/docs/i18n/th/llm.txt index 89e84411e9..cd0a37688c 100644 --- a/docs/i18n/th/llm.txt +++ b/docs/i18n/th/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/tr/CHANGELOG.md b/docs/i18n/tr/CHANGELOG.md index 8eaeb8134e..dbedcfce3d 100644 --- a/docs/i18n/tr/CHANGELOG.md +++ b/docs/i18n/tr/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/tr/llm.txt b/docs/i18n/tr/llm.txt index 32b0be9837..634d4bc259 100644 --- a/docs/i18n/tr/llm.txt +++ b/docs/i18n/tr/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/uk-UA/CHANGELOG.md b/docs/i18n/uk-UA/CHANGELOG.md index bcd4c5885b..8fb0302985 100644 --- a/docs/i18n/uk-UA/CHANGELOG.md +++ b/docs/i18n/uk-UA/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/uk-UA/llm.txt b/docs/i18n/uk-UA/llm.txt index e21defcb6a..14cf3b6600 100644 --- a/docs/i18n/uk-UA/llm.txt +++ b/docs/i18n/uk-UA/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/ur/CHANGELOG.md b/docs/i18n/ur/CHANGELOG.md index 718eefbabb..3421a2b5f2 100644 --- a/docs/i18n/ur/CHANGELOG.md +++ b/docs/i18n/ur/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/ur/llm.txt b/docs/i18n/ur/llm.txt index cac8e8c752..c402cff2a1 100644 --- a/docs/i18n/ur/llm.txt +++ b/docs/i18n/ur/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/vi/CHANGELOG.md b/docs/i18n/vi/CHANGELOG.md index 5e37cc493f..e4958494d7 100644 --- a/docs/i18n/vi/CHANGELOG.md +++ b/docs/i18n/vi/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/vi/llm.txt b/docs/i18n/vi/llm.txt index efe5b2c9cd..e21816296c 100644 --- a/docs/i18n/vi/llm.txt +++ b/docs/i18n/vi/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/zh-CN/CHANGELOG.md b/docs/i18n/zh-CN/CHANGELOG.md index 2b8a8ed467..13016311d6 100644 --- a/docs/i18n/zh-CN/CHANGELOG.md +++ b/docs/i18n/zh-CN/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/zh-CN/llm.txt b/docs/i18n/zh-CN/llm.txt index 4d3f47bfef..090969a4f0 100644 --- a/docs/i18n/zh-CN/llm.txt +++ b/docs/i18n/zh-CN/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/zh-TW/CHANGELOG.md b/docs/i18n/zh-TW/CHANGELOG.md index 65fefe097c..471cd182ca 100644 --- a/docs/i18n/zh-TW/CHANGELOG.md +++ b/docs/i18n/zh-TW/CHANGELOG.md @@ -6,7 +6,207 @@ ## [3.8.31] — 2026-06-20 -## [3.8.45] — TBD +## [3.8.46] — TBD + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. + +### 🐛 Bug Fixes + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### ⚡ Performance & Infrastructure + +- **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) +- **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) +- **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) + +--- + +## [3.8.45] — 2026-07-06 + +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) +- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`. +- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe) +- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma) +- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127) +- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`. +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + +### 🔧 Bug Fixes + +- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310)) +- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`. +- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites). +- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark) +- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config). +- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23. +- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333) +- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`. +- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah) +- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge. +- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun) +- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr) +- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun) +- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170)) +- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX) +- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd) +- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni) +- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega) +- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu) +- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211)) +- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208)) +- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165)) +- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164)) +- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161)) +- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158)) + +- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50). + +- **fix(nodejs):** the default app log path now resolves under `DATA_DIR` (`~/.omniroute/logs/application/app.log`) instead of `process.cwd()` ([#6197](https://github.com/diegosouzapw/OmniRoute/issues/6197)) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented `.env.example` default. `getAppLogFilePath()` now computes the default lazily via the pure `resolveDataDir()` resolver (honours a per-process `DATA_DIR`, no directory-creation side effect); an explicit `APP_LOG_FILE_PATH` still wins. Regression guard: `tests/unit/logenv-datadir-path-6197.test.ts` (3). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!) + +- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/6198)). The default DNS step (`addDNSEntry`) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (`USER node`, no `sudo`, read-only `/etc/hosts`) it threw `Command failed with code 1` out of `startMitmInternal` and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort `provisionDnsEntries()` where each failure is logged with the full `err` (stderr included, folded in by `systemCommands.ts`) and never aborts the start. Regression guard: `tests/unit/mitm-dns-graceful-degrade-6127.test.ts` (4). + +- **fix(providers):** copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty `content:null` stream ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)). Two gaps: (1) `buildWsUrl()` hardcoded the individual-consumer scenario (`OfficeWebPaidConsumerCopilot`, `isEdu=false`) — the EDU tier is now opt-in via `providerSpecificData.tier="edu"`, emitting `scenario=OfficeWebIncludedCopilot`/`isEdu=true` (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via `arguments[0].writeAtCursor` (incremental) instead of only `messages[].text` (accumulated snapshots), which the parser dropped — a new `accumulateBotContent()` folds both formats, with `type:2 item.result.message` as a last-resort fallback. Regression guard: `tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts` (10). (thanks @qpeyba) + +- **fix(providers):** GitLab Duo executor now feeds tool results back into the prompt instead of looping ([#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)) — `buildPrompt()` branched only on `system`/`user` and took `userParts.at(-1)`, silently dropping the `assistant{tool_calls}` + `tool{result}` turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same `` call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by its `tool_call_id`; simple conversations keep the legacy shape. Complements the tool_call emission from [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051) (the `kilo-duplicate` label was a false positive — different, sequential defect). Regression guard: `tests/unit/gitlab-tool-result-feedback-6220.test.ts` (4). + +- **fix(providers):** opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress ([#5997](https://github.com/diegosouzapw/OmniRoute/issues/5997)) — on a datacenter VPS, `opencode.ai/zen/go/v1/chat/completions` 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that `User-Agent: opencode-cli/1.0.0` + `x-opencode-client: cli` + `x-opencode-project: default` + fresh request/session UUIDs succeed. Opt-in via `OPENCODE_SYNTHESIZE_CLI_HEADERS=true` (values overridable via `OPENCODE_GO_USER_AGENT`/`OPENCODE_USER_AGENT`/`OPENCODE_CLIENT`/`OPENCODE_PROJECT`); it fills only headers the client did not already send. Kept **off by default** — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed with `opencode/local`), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard: `tests/unit/opencode-cli-headers-synthesis-5997.test.ts` (6). (thanks @aleksesipenko) +- fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219) + +- fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199) + +- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`. + +- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak) + +- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba) + +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen) +- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation) +- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN) +- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation) +- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127) +- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`. +- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif) +- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178)) + +### ⚡ Performance & Infrastructure + +- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files). +- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline). +- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation. +- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real). +- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref `. +- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`. +- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200. +- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3). + +### 📝 Maintenance + +- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes. +- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300)) +- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305)) +- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys. +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167)) +- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened. +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) +- **chore(providers):** remove deprecated **MiMo V2** model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops `mimo-v2-tts`, `mimo-v2-pro`, `mimo-v2-omni`, `mimo-v2-flash`, `mimo-v2-flash-free` and realigns the provider-catalog tests. ([#6248](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun) + +### 🙌 Contributors + +Thanks to everyone whose work landed in v3.8.45: + +| Contributor | PRs / Issues | +| ------------------------------------------------------------------ | ----------------------------------- | +| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | +| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report | +| [@anki1kr](https://github.com/anki1kr) | #6078 | +| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 | +| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 | +| [@chirag127](https://github.com/chirag127) | direct commit / report | +| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 | +| [@hartmark](https://github.com/hartmark) | #6216 | +| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 | +| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report | +| [@kanztu](https://github.com/kanztu) | #6181 | +| [@karimalsalah](https://github.com/karimalsalah) | #6291 | +| [@KooshaPari](https://github.com/KooshaPari) | #6166 | +| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 | +| [@luweiCN](https://github.com/luweiCN) | #6221 | +| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 | +| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | +| [@powellnorma](https://github.com/powellnorma) | direct commit / report | +| [@qpeyba](https://github.com/qpeyba) | direct commit / report | +| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | +| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | +| [@SeaXen](https://github.com/SeaXen) | direct commit / report | +| [@serverless83](https://github.com/serverless83) | #6212 | +| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) | +| [@tenshiak](https://github.com/tenshiak) | direct commit / report | +| [@Theadd](https://github.com/Theadd) | #6194, #6195 | +| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | +| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 | +| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- diff --git a/docs/i18n/zh-TW/llm.txt b/docs/i18n/zh-TW/llm.txt index ece4e5b336..891f389de1 100644 --- a/docs/i18n/zh-TW/llm.txt +++ b/docs/i18n/zh-TW/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 13280554b1..991c650227 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: OmniRoute API - version: 3.8.45 + version: 3.8.46 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/CLI-TOOLS.md b/docs/reference/CLI-TOOLS.md index 42529429d2..ef38bbeb82 100644 --- a/docs/reference/CLI-TOOLS.md +++ b/docs/reference/CLI-TOOLS.md @@ -648,7 +648,7 @@ omniroute providers validate # Local-only structural vali ### Recovery & Reset ```bash -omniroute reset-password # Reset the admin password (legacy alias still works) +omniroute reset-password # Reset the admin password (also: omniroute-reset-password) omniroute reset-encrypted-columns # Show warning + dry-run for encrypted credential reset omniroute reset-encrypted-columns --force # Actually null out encrypted credentials in SQLite ``` diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md index e288136d00..1ca37d603f 100644 --- a/docs/reference/ENVIRONMENT.md +++ b/docs/reference/ENVIRONMENT.md @@ -133,7 +133,7 @@ OmniRoute uses **SQLite** (via `better-sqlite3`) for all persistence. These vari | `OMNIROUTE_DISABLE_LIVE_WS` | `false` | `scripts/start-ws-server.mjs` | CI/harness toggle that disables the standalone live WebSocket helper script. | | `RELAY_IP_PER_MINUTE` | `30` | `src/app/api/v1/relay/chat/completions/route.ts` | Per-(token, IP) relay rate limit, requests/minute. In-memory, per instance. `0` or negative disables the IP-dimension gate (per-token DB limit still applies). | | `NODE_ENV` | `production` | Next.js core | Controls logging verbosity, caching, error detail exposure, and Next.js optimizations. | -| `OMNIROUTE_USE_TURBOPACK` | `1` (default in `.env.example`) | `package.json` / Next.js 16 | Toggles the Next.js 16 Turbopack bundler in `npm run dev` and `npm run build`. Set to `0` on Windows or when running into native binding incompatibilities. | +| `OMNIROUTE_USE_TURBOPACK` | `1` (Turbopack — code default) | `package.json` / Next.js 16 | Turbopack is the default bundler for `npm run dev` and `npm run build` (2-3× faster builds, benchmarked). Set to `0` to fall back to webpack on Windows or when running into native binding / bundler-compat incompatibilities. | | `OMNIROUTE_SKIP_DB_HEALTHCHECK` | _(unset)_ | `src/lib/db/core.ts` / `src/lib/db/healthCheck.ts` | Set to `1` to skip the SQLite integrity health check on startup. Useful for faster boot on large databases. | | `CREDENTIAL_HEALTH_CHECK_INTERVAL` | `300000` | `open-sse/config/constants.ts` / `src/lib/credentialHealth/scheduler.ts` | Interval (ms) for the background credential health check scheduler. Minimum: 10000 (10s). | | `CREDENTIAL_HEALTH_CACHE_TTL` | `300000` | `open-sse/config/constants.ts` / `src/lib/credentialHealth/cache.ts` | TTL (ms) for cached credential health status. | @@ -284,6 +284,10 @@ OmniRoute provides a two-layer defense: request-side injection scanning and resp | `OPENCODE_GO_WORKSPACE_ID` | _(unset)_ | `open-sse/services/usage.ts` | OpenCode Go workspace ID used for dashboard quota scraping. Prefer the per-connection Dashboard field when multiple accounts are configured. | | `OMNIROUTE_OPENCODE_GO_WORKSPACE_ID` | _(unset)_ | `open-sse/services/usage.ts` | Alternate OpenCode Go workspace ID env var used before the shorter alias. Prefer the per-connection Dashboard field when multiple accounts are configured. | | `OPENCODE_GO_AUTH_COOKIE` | _(unset)_ | `open-sse/services/usage.ts` | OpenCode Go `auth` cookie used for dashboard quota scraping. Sensitive; prefer the per-connection Dashboard field when multiple accounts are configured. | +| `OPENCODE_SYNTHESIZE_CLI_HEADERS` | `false` | `open-sse/executors/opencode.ts` | Opt-in: synthesize OpenCode CLI identity headers (User-Agent, x-opencode-client/project, request/session UUIDs) on opencode-go/zen upstream requests the client didn't send, so Cloudflare on VPS egress accepts them (#6210/#5997). Off by default (forward-only is safer). | +| `OPENCODE_USER_AGENT` | `opencode-cli/1.0.0` | `open-sse/executors/opencode.ts` | Default User-Agent used when `OPENCODE_SYNTHESIZE_CLI_HEADERS` is on and no per-provider `_USER_AGENT` override is set. Only applied to opencode executors. | +| `OPENCODE_CLIENT` | `cli` | `open-sse/executors/opencode.ts` | Value for the synthesized `x-opencode-client` header when `OPENCODE_SYNTHESIZE_CLI_HEADERS` is on. | +| `OPENCODE_PROJECT` | `default` | `open-sse/executors/opencode.ts` | Value for the synthesized `x-opencode-project` header when `OPENCODE_SYNTHESIZE_CLI_HEADERS` is on. | | `OMNIROUTE_OPENCODE_GO_AUTH_COOKIE` | _(unset)_ | `open-sse/services/usage.ts` | Alternate OpenCode Go `auth` cookie env var used before the shorter alias. Sensitive; prefer the per-connection Dashboard field when multiple accounts are configured. | | `OMNIROUTE_OLLAMA_CLOUD_USAGE_URL` | `https://ollama.com/settings` | `open-sse/services/usage.ts` | Ollama Cloud settings URL used for quota scraping. Override for relays / test fixtures. | | `OLLAMA_USAGE_COOKIE` | _(unset)_ | `open-sse/services/usage.ts` | Ollama Cloud `__Secure-session` cookie used for settings-page quota scraping. Sensitive; prefer the per-connection Dashboard field when multiple accounts are configured. | @@ -836,6 +840,7 @@ Anthropic-compatible provider instead. | `PROXY_HEALTH_ENABLED` | `true` | `src/lib/proxyHealth/scheduler.ts` | Set `false` to disable the background proxy health scheduler that periodically probes registered proxies. | | `PROXY_HEALTH_INTERVAL_MS` | `600000` | `src/lib/proxyHealth/scheduler.ts` | Background health-scheduler sweep interval in ms (minimum `60000`). | | `PROXY_HEALTH_TEST_URL` | `https://httpbin.org/ip` | `src/lib/proxyHealth/scheduler.ts` | Reachability probe target used by the scheduler and the `/api/settings/proxies/auto-test` endpoint. Point it at an internal/self-hosted URL to avoid the public default. | +| `PROXY_HEALTH_AUTO_DEACTIVATE` | `false` | `src/lib/proxyHealth/statusPolicy.ts` | When `false` (default), automated reachability probes (the scheduler + the `/api/settings/proxies/auto-test` "Test All" button) are **read-only** and never write a proxy's status — only the operator sets active/inactive, so a flaky probe can't strand an assigned proxy (#6246). Set `true` to restore the legacy test-and-set behaviour. | | `PROXY_AUTO_REMOVE` | `false` | `src/lib/proxyHealth/scheduler.ts` | Set `true` to let the scheduler auto-remove proxies after repeated consecutive failures. | | `PROXY_AUTO_REMOVE_AFTER` | `3` | `src/lib/proxyHealth/scheduler.ts` | Consecutive failures before the scheduler auto-removes a proxy (when `PROXY_AUTO_REMOVE=true`). | | `OMNIROUTE_CONTROL_PLANE_PROXY_DIRECT_FALLBACK` | `false` | `src/shared/constants/featureFlagDefinitions.ts` | Allow OAuth and provider validation flows to bypass a pinned proxy and connect directly when proxy reachability pre-checks fail. Effective precedence is Feature Flags DB override > env var > default. | @@ -1008,6 +1013,10 @@ Provider quota endpoints, network tunnels (Tailscale, Ngrok, MITM debug proxy), | `MITM_DISABLE_TLS_VERIFY` | `0` | `src/mitm/server.cjs` | Set `1` to disable upstream TLS verification (development only). | | `MITM_IDLE_TIMEOUT_MS` | `60000` | `src/mitm/socketTimeouts.ts`, `src/mitm/server.cjs` | Idle socket timeout (ms) for proxied connections; idle sockets past this are torn down to avoid leaking half-open tunnels. | | `MITM_VERBOSE` | `1` | `src/mitm/server.cjs`, `src/mitm/_internal/bypass.cjs` | Routing-decision log verbosity: `0` silences, higher values log more bypass/route decisions. | +| `OMNIROUTE_NO_SUDO` | `0` | `src/mitm/systemCommands.ts` | Set `1` (truthy) to strip the leading `sudo` from MITM cert-trust commands — for root-less / user-namespaced deployments where the operator trusts the CA manually (e.g. via Node's extra-CA-certs mechanism). | +| `OMNIROUTE_SKIP_SYSTEM_TRUST` | `0` | `src/mitm/cert/install.ts`, `src/mitm/tproxy/caTrust.ts` | Test/CI-only guard: set `1` to make cert trust install/uninstall a no-op so the suite never mutates the OS trust store. Set automatically by the test setup and CI workflows. | +| `CHANGELOG_BASE_REF` | _(auto)_ | `scripts/check/check-changelog-integrity.mjs` | Explicit base ref for the anti CHANGELOG-eat gate (defaults to the PR base branch in CI, or the highest `release/v*`). | +| `ALLOW_CHANGELOG_REMOVALS` | `0` | `scripts/check/check-changelog-integrity.mjs` | Set `1` to turn intentional CHANGELOG bullet removals into a report instead of a failure (justify in the PR body). | | `ONEPROXY_ENABLED` | `true` | `src/lib/oneproxySync.ts` | Enable the 1Proxy egress pool sync. | | `ONEPROXY_API_URL` | `https://1proxy-api.aitradepulse.com` | `src/lib/oneproxySync.ts` | 1Proxy service API URL override. | | `ONEPROXY_MAX_PROXIES` | `500` | `src/lib/oneproxySync.ts` | Maximum proxies imported per sync. | diff --git a/electron/package-lock.json b/electron/package-lock.json index ab8dfefbca..ed7a492715 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -1,12 +1,12 @@ { "name": "omniroute-desktop", - "version": "3.8.45", + "version": "3.8.46", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "omniroute-desktop", - "version": "3.8.45", + "version": "3.8.46", "license": "MIT", "dependencies": { "electron-updater": "^6.8.9" diff --git a/electron/package.json b/electron/package.json index c53246c201..90f35b9b11 100644 --- a/electron/package.json +++ b/electron/package.json @@ -1,6 +1,6 @@ { "name": "omniroute-desktop", - "version": "3.8.45", + "version": "3.8.46", "description": "OmniRoute Desktop Application", "main": "main.js", "author": { diff --git a/llm.txt b/llm.txt index 8ba39f8e01..d2995c50a4 100644 --- a/llm.txt +++ b/llm.txt @@ -14,7 +14,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -102,7 +102,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -124,7 +124,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -435,7 +435,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (45+ domain-specific files, 55 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (95+ domain-specific files, 110+ migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: 75% statements/lines/functions, 70% branches. diff --git a/next.config.mjs b/next.config.mjs index 415f5b986e..7fb83187b5 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -2,6 +2,7 @@ import createNextIntlPlugin from "next-intl/plugin"; import { createMDX } from "fumadocs-mdx/next"; import { dirname } from "node:path"; import { fileURLToPath } from "node:url"; +import { mitmManagerAliasFor } from "./scripts/build/mitm-stub-flag.mjs"; const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts"); const distDir = process.env.NEXT_DIST_DIR || ".build/next"; @@ -106,8 +107,13 @@ const nextConfig = { turbopack: { root: projectRoot, resolveAlias: { - // Point mitm/manager to a stub during build (native child_process/fs can't be bundled) - "@/mitm/manager": "./src/mitm/manager.stub.ts", + // @/mitm/manager → stub ONLY where the runtime can't run the MITM stack + // (Docker sets OMNIROUTE_MITM_STUB=1 — #3390 graceful degradation). The + // alias used to be unconditional, which was fine while Docker was the + // only Turbopack consumer — but the v3.8.45 bundler-default flip shipped + // the stub to every npm/Electron/VPS artifact and broke Agent Bridge + // start for all non-Docker users (#6344). See scripts/build/mitm-stub-flag.mjs. + ...mitmManagerAliasFor(process.env), ...minimalBuildAliases, }, }, diff --git a/open-sse/config/audioRegistry.ts b/open-sse/config/audioRegistry.ts index cbeaa789e5..144d0f563f 100644 --- a/open-sse/config/audioRegistry.ts +++ b/open-sse/config/audioRegistry.ts @@ -416,7 +416,6 @@ export const AUDIO_SPEECH_PROVIDERS: Record = { { id: "mimo-v2.5-tts", name: "MiMo V2.5 TTS" }, { id: "mimo-v2.5-tts-voicedesign", name: "MiMo V2.5 Voice Design" }, { id: "mimo-v2.5-tts-voiceclone", name: "MiMo V2.5 Voice Clone" }, - { id: "mimo-v2-tts", name: "MiMo V2 TTS" }, ], }, }; @@ -512,10 +511,7 @@ export function parseSpeechModel(modelStr: string | null, dynamicProviders?: Aud return parseAudioModel(modelStr, AUDIO_SPEECH_PROVIDERS, dynamicProviders); } -export function parseTranslationModel( - modelStr: string | null, - dynamicProviders?: AudioProvider[] -) { +export function parseTranslationModel(modelStr: string | null, dynamicProviders?: AudioProvider[]) { return parseAudioModel(modelStr, AUDIO_TRANSLATION_PROVIDERS, dynamicProviders); } diff --git a/open-sse/config/providerFieldStrips.ts b/open-sse/config/providerFieldStrips.ts index 0cb5f3f8f5..5a342cc9c1 100644 --- a/open-sse/config/providerFieldStrips.ts +++ b/open-sse/config/providerFieldStrips.ts @@ -1,10 +1,16 @@ // Fields that, when literally named in an upstream 400 body, are safe to strip and // retry once (FCC NIM-style recovery). Mirrors the existing context_management 400 // fallback in base.ts, generalized to these OpenAI-compat / NIM reasoning fields. +// `context_management` (9router#1468): Claude Code sends it top-level; strict +// anthropic-compatible gateways 400 with "context_management: Extra inputs are not +// permitted". The dedicated base.ts fallback only fires when OmniRoute's own +// contextEditing feature is enabled, so a client-sent field passed through +// untouched when the feature is off — this generic strip covers that case. export const KNOWN_OFFENDING_FIELDS: readonly string[] = [ "reasoning_budget", "chat_template", "reasoning_content", + "context_management", ]; /** Return the first known-offending field literally named in a 400 body, or null. */ diff --git a/open-sse/config/providers/index.ts b/open-sse/config/providers/index.ts index 4e21b903ce..5e49767b63 100644 --- a/open-sse/config/providers/index.ts +++ b/open-sse/config/providers/index.ts @@ -135,6 +135,7 @@ import { agentrouterProvider } from "./registry/agentrouter/index.ts"; import { zaiProvider } from "./registry/zai/index.ts"; import { waferProvider } from "./registry/wafer/index.ts"; import { huggingchatProvider } from "./registry/huggingchat/index.ts"; +import { yuanbao_webProvider } from "./registry/yuanbao-web/index.ts"; import { galadrielProvider } from "./registry/galadriel/index.ts"; import { qianfanProvider } from "./registry/qianfan/index.ts"; import { meta_llamaProvider } from "./registry/meta-llama/index.ts"; @@ -182,6 +183,9 @@ import { zenmux_freeProvider } from "./registry/zenmux-free/index.ts"; import { sumopodProvider } from "./registry/sumopod/index.ts"; import { x5labProvider } from "./registry/x5lab/index.ts"; import { kenariProvider } from "./registry/kenari/index.ts"; +import { requestyProvider } from "./registry/requesty/index.ts"; +import { digitaloceanProvider } from "./registry/digitalocean/index.ts"; +import { hcnsecProvider } from "./registry/hcnsec/index.ts"; export const REGISTRY: Record = { aimlapi: aimlapiProvider, @@ -316,6 +320,7 @@ export const REGISTRY: Record = { agentrouter: agentrouterProvider, zai: zaiProvider, huggingchat: huggingchatProvider, + "yuanbao-web": yuanbao_webProvider, galadriel: galadrielProvider, qianfan: qianfanProvider, "meta-llama": meta_llamaProvider, @@ -366,4 +371,7 @@ export const REGISTRY: Record = { sumopod: sumopodProvider, x5lab: x5labProvider, kenari: kenariProvider, + requesty: requestyProvider, + digitalocean: digitaloceanProvider, + hcnsec: hcnsecProvider, }; diff --git a/open-sse/config/providers/registry/agentrouter/index.ts b/open-sse/config/providers/registry/agentrouter/index.ts index a4156d76ca..ebe9d4598f 100644 --- a/open-sse/config/providers/registry/agentrouter/index.ts +++ b/open-sse/config/providers/registry/agentrouter/index.ts @@ -1,14 +1,4 @@ import type { RegistryEntry } from "../../shared.ts"; -import { - getClaudeCliHeaders, - mapStainlessOs, - mapStainlessArch, - ANTHROPIC_BETA_CLAUDE_OAUTH, - ANTHROPIC_VERSION_HEADER, - CLAUDE_CLI_STAINLESS_PACKAGE_VERSION, - CLAUDE_CLI_STAINLESS_RUNTIME_VERSION, - CLAUDE_CLI_USER_AGENT, -} from "../../shared.ts"; export const agentrouterProvider: RegistryEntry = { id: "agentrouter", @@ -19,7 +9,11 @@ export const agentrouterProvider: RegistryEntry = { authType: "apikey", authHeader: "x-api-key", defaultContextLength: 128000, - headers: getClaudeCliHeaders(), + // No static `headers` here: agentrouter now adopts the DYNAMIC Claude-Code + // wire image via CC_WIRE_IMAGE_BUILTINS (#6056) — the fingerprint/headers are + // applied by buildProviderHeaders + applyFingerprint, keeping this entry's + // own baseUrl + x-api-key auth. A static fingerprint here would drift and + // trip AgentRouter's WAF ("unauthorized client detected"). models: [ { id: "claude-opus-4-6", name: "Claude 4.6 Opus" }, { id: "claude-haiku-4-5-20251001", name: "Claude 4.5 Haiku" }, diff --git a/open-sse/config/providers/registry/cerebras/index.ts b/open-sse/config/providers/registry/cerebras/index.ts index d9ea106b90..01c337f691 100644 --- a/open-sse/config/providers/registry/cerebras/index.ts +++ b/open-sse/config/providers/registry/cerebras/index.ts @@ -10,6 +10,7 @@ export const cerebrasProvider: RegistryEntry = { authHeader: "bearer", models: [ { id: "zai-glm-4.7", name: "GLM 4.7" }, + { id: "gemma-4-31b", name: "Gemma 4 31B" }, { id: "gpt-oss-120b", name: "GPT OSS 120B" }, ], }; diff --git a/open-sse/config/providers/registry/digitalocean/index.ts b/open-sse/config/providers/registry/digitalocean/index.ts new file mode 100644 index 0000000000..af1186934c --- /dev/null +++ b/open-sse/config/providers/registry/digitalocean/index.ts @@ -0,0 +1,11 @@ +import type { RegistryEntry } from "../../shared.ts"; +import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts"; + +export const digitaloceanProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({ + id: "digitalocean", + alias: "digitalocean", + baseUrl: "https://inference.do-ai.run/v1/chat/completions", + modelsUrl: "https://inference.do-ai.run/v1/models", + models: [], + passthroughModels: true, +}); diff --git a/open-sse/config/providers/registry/hcnsec/index.ts b/open-sse/config/providers/registry/hcnsec/index.ts new file mode 100644 index 0000000000..acce62c2bc --- /dev/null +++ b/open-sse/config/providers/registry/hcnsec/index.ts @@ -0,0 +1,11 @@ +import type { RegistryEntry } from "../../shared.ts"; +import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts"; + +export const hcnsecProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({ + id: "hcnsec", + alias: "hcnsec", + baseUrl: "https://api.hcnsec.cn/v1/chat/completions", + modelsUrl: "https://api.hcnsec.cn/v1/models", + models: [], + passthroughModels: true, +}); diff --git a/open-sse/config/providers/registry/opencode/go/index.ts b/open-sse/config/providers/registry/opencode/go/index.ts index b1c0b4f12e..c8e575b185 100644 --- a/open-sse/config/providers/registry/opencode/go/index.ts +++ b/open-sse/config/providers/registry/opencode/go/index.ts @@ -25,8 +25,6 @@ export const opencode_goProvider: RegistryEntry = { { id: "kimi-k2.5", name: "Kimi K2.5" }, { id: "mimo-v2.5-pro", name: "MiMo-V2.5-Pro" }, { id: "mimo-v2.5", name: "MiMo-V2.5" }, - { id: "mimo-v2-pro", name: "MiMo-V2-Pro" }, - { id: "mimo-v2-omni", name: "MiMo-V2-Omni" }, // #3110: MiniMax M3 via OpenCode Go tier { id: "minimax-m3", diff --git a/open-sse/config/providers/registry/requesty/index.ts b/open-sse/config/providers/registry/requesty/index.ts new file mode 100644 index 0000000000..6fc063bce2 --- /dev/null +++ b/open-sse/config/providers/registry/requesty/index.ts @@ -0,0 +1,11 @@ +import type { RegistryEntry } from "../../shared.ts"; +import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts"; + +export const requestyProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({ + id: "requesty", + alias: "requesty", + baseUrl: "https://router.requesty.ai/v1/chat/completions", + modelsUrl: "https://router.requesty.ai/v1/models", + models: [], + passthroughModels: true, +}); diff --git a/open-sse/config/providers/registry/yuanbao-web/index.ts b/open-sse/config/providers/registry/yuanbao-web/index.ts new file mode 100644 index 0000000000..c3dc4b1565 --- /dev/null +++ b/open-sse/config/providers/registry/yuanbao-web/index.ts @@ -0,0 +1,37 @@ +import type { RegistryEntry } from "../../shared.ts"; + +export const yuanbao_webProvider: RegistryEntry = { + id: "yuanbao-web", + alias: "ybw", + format: "openai", + executor: "yuanbao-web", + baseUrl: "https://yuanbao.tencent.com/api/chat", + authType: "apikey", + authHeader: "cookie", + models: [ + { id: "deepseek-v3", name: "DeepSeek V3 (via Yuanbao)", toolCalling: false }, + { + id: "deepseek-r1", + name: "DeepSeek R1 (via Yuanbao)", + supportsReasoning: true, + }, + { id: "hunyuan", name: "Hunyuan (via Yuanbao)" }, + { + id: "hunyuan-t1", + name: "Hunyuan T1 (via Yuanbao)", + supportsReasoning: true, + }, + { id: "deepseek-v3-search", name: "DeepSeek V3 + Web Search (via Yuanbao)" }, + { + id: "deepseek-r1-search", + name: "DeepSeek R1 + Web Search (via Yuanbao)", + supportsReasoning: true, + }, + { id: "hunyuan-search", name: "Hunyuan + Web Search (via Yuanbao)" }, + { + id: "hunyuan-t1-search", + name: "Hunyuan T1 + Web Search (via Yuanbao)", + supportsReasoning: true, + }, + ], +}; diff --git a/open-sse/config/providers/registry/zenmux-free/index.ts b/open-sse/config/providers/registry/zenmux-free/index.ts index 126e2bf203..21ff0139bd 100644 --- a/open-sse/config/providers/registry/zenmux-free/index.ts +++ b/open-sse/config/providers/registry/zenmux-free/index.ts @@ -9,7 +9,7 @@ import type { RegistryEntry } from "../../shared.ts"; * for all API requests as a query parameter. * * Models available on the free tier (5 Flows/5h, 38.64 Flows/week): - * DeepSeek V3.2, GLM 4.7 Flash Free, MiMo V2 Flash Free, and others. + * DeepSeek V3.2, GLM 4.7 Flash Free, and others. * * Short alias "zmf" is distinct from the paid "zenmux" (alias "zm") which * uses API-key auth against the OpenAI-compatible endpoint. @@ -27,7 +27,6 @@ export const zenmux_freeProvider: RegistryEntry = { { id: "deepseek/deepseek-reasoner", name: "DeepSeek V3.2 (Thinking)", supportsReasoning: true }, { id: "deepseek/deepseek-v4-pro", name: "DeepSeek V4 Pro", supportsReasoning: true }, { id: "kuaishou/kat-coder-pro-v1-free", name: "KAT Coder Pro V1 Free" }, - { id: "xiaomi/mimo-v2-flash-free", name: "MiMo V2 Flash Free" }, { id: "z-ai/glm-4.7-flash-free", name: "GLM 4.7 Flash Free" }, { id: "stepfun/step-3.5-flash-free", name: "Step 3.5 Flash Free" }, { id: "inclusionai/ling-1t", name: "Ling 1T" }, diff --git a/open-sse/config/providers/shared.ts b/open-sse/config/providers/shared.ts index 1a90d258e6..4059673697 100644 --- a/open-sse/config/providers/shared.ts +++ b/open-sse/config/providers/shared.ts @@ -435,9 +435,6 @@ export const CHAT_OPENAI_COMPAT_MODELS: Record = { "xiaomi-mimo": [ { id: "mimo-v2.5-pro", name: "MiMo-V2.5-Pro", contextLength: 1048576, maxOutputTokens: 131072 }, { id: "mimo-v2.5", name: "MiMo-V2.5", contextLength: 1048576, maxOutputTokens: 131072 }, - { id: "mimo-v2-pro", name: "MiMo-V2-Pro", contextLength: 262144, maxOutputTokens: 131072 }, - { id: "mimo-v2-omni", name: "MiMo-V2-Omni", contextLength: 262144, maxOutputTokens: 131072 }, - { id: "mimo-v2-flash", name: "MiMo-V2-Flash", contextLength: 262144, maxOutputTokens: 65536 }, ], gitlawb: [ { id: "mimo-v2.5-pro", name: "MiMo-V2.5-Pro", contextLength: 1048576, maxOutputTokens: 131072 }, diff --git a/open-sse/executors/antigravity.ts b/open-sse/executors/antigravity.ts index a66f58df06..5489e9fbb9 100644 --- a/open-sse/executors/antigravity.ts +++ b/open-sse/executors/antigravity.ts @@ -480,6 +480,46 @@ function sanitizeAntigravityGeminiRequest( return clean; } +/** + * Ported from decolua/9router#2321 (anki1kr): Vertex AI (used by Antigravity for + * Claude-branded models) rejects a conversation ending on an assistant turn — + * "This model does not support assistant message prefill" — so the request must + * always end on a user turn. Upstream patched `openaiToClaudeRequestForAntigravity` + * (dead code here, zero callers — see `open-sse/translator/request/openai-to-claude.ts`); + * this relocates the same strip to the LIVE Antigravity dispatch path, where Claude + * requests are converted to Gemini `contents` (assistant role is `"model"`, not + * `"assistant"`). Mirrors the trailing-strip pop-loop already used for Mistral + * (#3396), Copilot (#5802), and the CC-bridge in `claudeCodeCompatible.ts`. + * + * Scoped strictly to the Claude path by the caller (`isClaude` branch only) — native + * Gemini models via Antigravity must be unaffected, since Vertex-Claude is the only + * documented rejection surface. + * + * Guard: never strip `contents` down to empty — an empty `contents` array is itself + * an invalid request, so at least one entry (even a lone trailing "model" turn) is + * always preserved. + */ +function stripTrailingAntigravityAssistantTurn( + request: Record +): Record { + const contents = request.contents; + if (!Array.isArray(contents) || contents.length === 0) { + return request; + } + + while ( + contents.length > 1 && + (contents[contents.length - 1] as AntigravityContent)?.role === "model" + ) { + contents.pop(); + } + + return request; +} + +// Test-only export so the unit suite can exercise the strip logic directly. +export const __test_stripTrailingAntigravityAssistantTurn = stripTrailingAntigravityAssistantTurn; + export class AntigravityExecutor extends BaseExecutor { constructor() { super("antigravity", PROVIDERS.antigravity); @@ -660,7 +700,7 @@ export class AntigravityExecutor extends BaseExecutor { }; const transformedRequest = isClaude - ? sanitizeAntigravityGeminiRequest(rawTransformedRequest) + ? stripTrailingAntigravityAssistantTurn(sanitizeAntigravityGeminiRequest(rawTransformedRequest)) : rawTransformedRequest; // Obfuscate sensitive client names in user content (e.g. "OpenCode", "Cursor") diff --git a/open-sse/executors/copilot-m365-connection.ts b/open-sse/executors/copilot-m365-connection.ts index e5545de3f9..80a06a75ec 100644 --- a/open-sse/executors/copilot-m365-connection.ts +++ b/open-sse/executors/copilot-m365-connection.ts @@ -24,6 +24,30 @@ export const M365_INDIVIDUAL_DEFAULTS = { scenario: "OfficeWebPaidConsumerCopilot", } as const; +/** + * Education "Starter / OfficeWebIncludedCopilot" tier overrides, captured from the + * official UI in #6210. Differs from the individual tier only by scenario + isEdu; + * opt-in via `providerSpecificData.tier="edu"` so the individual path is unchanged. + */ +export const M365_EDU_OVERRIDES = { + scenario: "OfficeWebIncludedCopilot", + isEdu: "true", + licenseType: "Starter", +} as const; + +/** + * Enterprise / "work" (Microsoft 365 Copilot for work) tier overrides (#6334). Enterprise + * tenants ride the `agent="work"` BizChat surface with the `officeweb` scenario and a + * Premium license. Opt-in via `providerSpecificData.tier="enterprise"` (alias `"work"`) so + * the individual and EDU paths are unchanged. A raw `providerSpecificData.agent` override is + * also honored for tenants that need a different agent value. + */ +export const M365_ENTERPRISE_OVERRIDES = { + agent: "work", + scenario: "officeweb", + licenseType: "Premium", +} as const; + export const M365_DEFAULT_VARIANTS = [ "EnableMcpServerWidgets", "feature.EnableMcpServerWidgets", @@ -79,6 +103,11 @@ export interface M365ConnectionParams { chathubPath: string; // "@" accessToken: string; variants?: string; + /** Tier overrides — when unset, buildWsUrl falls back to the individual defaults. */ + scenario?: string; + isEdu?: string; + licenseType?: string; + agent?: string; } /** A new 32-hex chat session id (== XRoutingParameterSessionKey == clientrequestid). */ @@ -154,7 +183,41 @@ export function resolveConnectionParams( } const host = (typeof psd.host === "string" && psd.host) || M365_INDIVIDUAL_DEFAULTS.host; const variants = typeof psd.variants === "string" && psd.variants ? psd.variants : undefined; - return { host, chathubPath, accessToken, variants }; + + return { host, chathubPath, accessToken, variants, ...resolveTierOverrides(psd) }; +} + +/** + * Resolve tier overrides (opt-in). `tier="edu"|"included"` applies the EDU overrides and + * `tier="enterprise"|"work"` applies the enterprise/work overrides; individual fields + * (`scenario`/`isEdu`/`licenseType`/`agent`) can also be overridden directly via + * providerSpecificData. Unset fields fall back to the individual defaults in buildWsUrl. + * (#6210, #6334) + */ +function resolveTierOverrides( + psd: JsonRecord +): Pick { + const tier = typeof psd.tier === "string" ? psd.tier.toLowerCase() : ""; + const isEduTier = tier === "edu" || tier === "included"; + const isEnterpriseTier = tier === "enterprise" || tier === "work"; + const psdIsEdu = + (typeof psd.isEdu === "string" && psd.isEdu) || + (typeof psd.isEdu === "boolean" && String(psd.isEdu)) || + undefined; + return { + scenario: + (typeof psd.scenario === "string" && psd.scenario) || + (isEduTier ? M365_EDU_OVERRIDES.scenario : undefined) || + (isEnterpriseTier ? M365_ENTERPRISE_OVERRIDES.scenario : undefined), + isEdu: psdIsEdu || (isEduTier ? M365_EDU_OVERRIDES.isEdu : undefined), + licenseType: + (typeof psd.licenseType === "string" && psd.licenseType) || + (isEduTier ? M365_EDU_OVERRIDES.licenseType : undefined) || + (isEnterpriseTier ? M365_ENTERPRISE_OVERRIDES.licenseType : undefined), + agent: + (typeof psd.agent === "string" && psd.agent) || + (isEnterpriseTier ? M365_ENTERPRISE_OVERRIDES.agent : undefined), + }; } /** @@ -175,10 +238,10 @@ export function buildWsUrl(params: M365ConnectionParams): string { source: M365_INDIVIDUAL_DEFAULTS.source, product: M365_INDIVIDUAL_DEFAULTS.product, agentHost: M365_INDIVIDUAL_DEFAULTS.agentHost, - licenseType: M365_INDIVIDUAL_DEFAULTS.licenseType, - isEdu: "false", - agent: M365_INDIVIDUAL_DEFAULTS.agent, - scenario: M365_INDIVIDUAL_DEFAULTS.scenario, + licenseType: params.licenseType ?? M365_INDIVIDUAL_DEFAULTS.licenseType, + isEdu: params.isEdu ?? "false", + agent: params.agent ?? M365_INDIVIDUAL_DEFAULTS.agent, + scenario: params.scenario ?? M365_INDIVIDUAL_DEFAULTS.scenario, }); return `wss://${params.host}/m365Copilot/Chathub/${params.chathubPath}?${query.toString()}`; } diff --git a/open-sse/executors/copilot-m365-frames.ts b/open-sse/executors/copilot-m365-frames.ts index 267b897c51..d4544aea20 100644 --- a/open-sse/executors/copilot-m365-frames.ts +++ b/open-sse/executors/copilot-m365-frames.ts @@ -228,3 +228,52 @@ export function incrementalDelta(previous: string, next: string): string { if (next.startsWith(previous)) return next.slice(previous.length); return next; } + +/** + * Extract an incremental `writeAtCursor` delta from a `type:1` update frame. The EDU / + * GPT-5.5 path (`OfficeWebIncludedCopilot`, feature.bizchatfluxv3) streams response text + * as `arguments[0].writeAtCursor` INCREMENTS instead of only accumulated `messages[].text` + * snapshots. Returns null when the frame carries no writeAtCursor delta. (#6210) + */ +export function extractWriteAtCursor(frame: Record | null): string | null { + if (!isUpdateFrame(frame)) return null; + const args = (frame as Record).arguments; + const first = Array.isArray(args) ? (args[0] as Record | undefined) : undefined; + const wac = first?.writeAtCursor; + return typeof wac === "string" && wac.length > 0 ? wac : null; +} + +/** + * Extract the final answer from a `type:2` invocation-result frame + * (`item.result.message`). Used as a last-resort fallback when a turn emitted no + * streamed content (some EDU turns only surface the answer here). (#6210) + */ +export function extractFinalResultMessage(frame: Record | null): string | null { + if (!frame || frame.type !== 2) return null; + const item = frame.item as Record | undefined; + const result = item?.result as Record | undefined; + const message = result?.message; + return typeof message === "string" && message.length > 0 ? message : null; +} + +/** + * Fold a single incoming frame into the running bot answer, returning the suffix to + * stream (`delta`) and the new accumulated text (`next`). Handles both wire formats: + * `messages[].text` snapshots are the full accumulated answer (diffed via + * {@link incrementalDelta}), while `writeAtCursor` frames are incremental and are + * appended. Non-content frames leave the state unchanged. (#6210) + */ +export function accumulateBotContent( + previous: string, + frame: Record | null +): { delta: string; next: string } { + const snapshot = extractBotText(frame); + if (snapshot) { + return { delta: incrementalDelta(previous, snapshot), next: snapshot }; + } + const wac = extractWriteAtCursor(frame); + if (wac) { + return { delta: wac, next: previous + wac }; + } + return { delta: "", next: previous }; +} diff --git a/open-sse/executors/copilot-m365-web.ts b/open-sse/executors/copilot-m365-web.ts index 7b7470c4a2..4a850a9804 100644 --- a/open-sse/executors/copilot-m365-web.ts +++ b/open-sse/executors/copilot-m365-web.ts @@ -1,7 +1,7 @@ import WebSocket from "ws"; import { FETCH_TIMEOUT_MS } from "../config/constants.ts"; import { sanitizeErrorMessage } from "../utils/error.ts"; -import { BaseExecutor, type ExecuteInput } from "./base.ts"; +import { BaseExecutor, type ExecuteInput, type ExecutorLog } from "./base.ts"; import { buildPrompt, buildWsUrl, @@ -9,12 +9,12 @@ import { resolveConnectionParams, } from "./copilot-m365-connection.ts"; import { + accumulateBotContent, buildChatInvocation, encodeFrame, - extractBotText, + extractFinalResultMessage, handshakeError, handshakeFrame, - incrementalDelta, isCompletionFrame, keepaliveFrame, parseFrame, @@ -59,7 +59,11 @@ export class CopilotM365WebExecutor extends BaseExecutor { prompt: string; model: string; signal?: AbortSignal; + log?: ExecutorLog | null; }): Promise> { + // #6210 — observability for the empty-response class. The access_token rides + // in the WS query string, so every URL logged here goes through redactWsUrl(). + const log = input.log ?? null; return new ReadableStream( { start: async (controller) => { @@ -68,6 +72,7 @@ export class CopilotM365WebExecutor extends BaseExecutor { let settled = false; let buffer = ""; let previousText = ""; + let finalResultMessage = ""; let handshakeComplete = false; const cleanup = () => { @@ -85,6 +90,13 @@ export class CopilotM365WebExecutor extends BaseExecutor { if (settled) return; settled = true; cleanup(); + // Last-resort fallback (#6210): some EDU turns surface the answer only in the + // type:2 invocation result. Emit it if nothing was streamed. + if (!previousText && finalResultMessage) { + controller.enqueue( + encoder.encode(sseChunk(input.model, { content: finalResultMessage })) + ); + } controller.enqueue(encoder.encode(sseChunk(input.model, {}, "stop"))); controller.enqueue(encoder.encode("data: [DONE]\n\n")); controller.close(); @@ -111,6 +123,8 @@ export class CopilotM365WebExecutor extends BaseExecutor { const traceId = wsUrlParts.searchParams.get("clientrequestid") ?? crypto.randomUUID().replace(/-/g, ""); const sessionId = wsUrlParts.searchParams.get("X-SessionId") ?? crypto.randomUUID(); + log?.debug?.("M365_WS", `connecting → ${redactWsUrl(input.wsUrl)}`); + ws = new WebSocketCtor(input.wsUrl, { headers: { Origin: "https://m365.cloud.microsoft", @@ -134,6 +148,7 @@ export class CopilotM365WebExecutor extends BaseExecutor { }; ws.on("open", () => { + log?.debug?.("M365_WS", "socket open — sending handshake"); ws?.send(handshakeFrame()); }); @@ -145,25 +160,33 @@ export class CopilotM365WebExecutor extends BaseExecutor { for (const rawFrame of split.frames) { const frame = parseFrame(rawFrame); + log?.debug?.( + "M365_WS", + `frame type=${String(frame?.type)} target=${String(frame?.target)}` + ); if (!handshakeComplete) { const err = handshakeError(frame); if (err) { clearTimeout(timeout); + log?.debug?.("M365_WS", `handshake failed: ${err}`); abort(`Microsoft 365 Copilot handshake failed: ${err}`); return; } handshakeComplete = true; + log?.debug?.("M365_WS", "handshake complete — sending chat invocation"); sendChat(); continue; } - const text = extractBotText(frame); - if (text) { - const delta = incrementalDelta(previousText, text); - previousText = text; - if (delta) { - controller.enqueue(encoder.encode(sseChunk(input.model, { content: delta }))); - } + const { delta, next } = accumulateBotContent(previousText, frame); + previousText = next; + if (delta) { + controller.enqueue(encoder.encode(sseChunk(input.model, { content: delta }))); + } + + const finalMsg = extractFinalResultMessage(frame); + if (finalMsg) { + finalResultMessage = finalMsg; } if (isCompletionFrame(frame)) { @@ -176,6 +199,10 @@ export class CopilotM365WebExecutor extends BaseExecutor { ws.on("error", (err) => { clearTimeout(timeout); + log?.debug?.( + "M365_WS", + `socket error: ${err instanceof Error ? err.message : String(err)}` + ); abort( sanitizeErrorMessage( err instanceof Error ? err.message : "Microsoft 365 Copilot WebSocket error" @@ -234,7 +261,13 @@ export class CopilotM365WebExecutor extends BaseExecutor { const wsUrl = buildWsUrl(connectionParams); try { - const wsStream = await this.wsChat({ wsUrl, prompt, model, signal: input.signal ?? undefined }); + const wsStream = await this.wsChat({ + wsUrl, + prompt, + model, + signal: input.signal ?? undefined, + log: input.log, + }); if (stream) { return { diff --git a/open-sse/executors/default.ts b/open-sse/executors/default.ts index afabd75645..b2697a349e 100644 --- a/open-sse/executors/default.ts +++ b/open-sse/executors/default.ts @@ -17,6 +17,10 @@ import { import { isOfficialAnthropicBaseUrl } from "../utils/anthropicHost.ts"; import { applyProviderRequestDefaults } from "../services/providerRequestDefaults.ts"; import { stripUnsupportedParams } from "../translator/paramSupport.ts"; +import { + injectReasoningContentForThinkingModel, + isThinkingMessageModel, +} from "../utils/reasoningContentInjector.ts"; import { detectFormat, getOpenAICompatibleType, @@ -545,8 +549,9 @@ export class DefaultExecutor extends BaseExecutor { withDefaults = this.defaultResponsesTextFormat(withDefaults); // Port of decolua/9router commit d652300e: - // Cerebras returns 400 (wrong_api_format) and Mistral returns 422 - // (extra_forbidden) when the forwarded body carries `client_metadata` + // Cerebras returns 400 (wrong_api_format), Mistral returns 422 + // (extra_forbidden), and NVIDIA's OpenAI-compatible wrapper returns 400 + // (Unsupported parameter) when the forwarded body carries `client_metadata` // (an OpenAI Codex / Claude CLI passthrough field with no equivalent on // these upstreams). Strip it before sending downstream. Other providers // (notably `openai` / `codex`) intentionally keep it. @@ -554,7 +559,9 @@ export class DefaultExecutor extends BaseExecutor { withDefaults && typeof withDefaults === "object" && !Array.isArray(withDefaults) && - (this.provider === "cerebras" || this.provider === "mistral") && + (this.provider === "cerebras" || + this.provider === "mistral" || + this.provider === "nvidia") && Object.prototype.hasOwnProperty.call(withDefaults, "client_metadata") ) { const withoutClientMetadata = { ...(withDefaults as Record) }; @@ -562,6 +569,41 @@ export class DefaultExecutor extends BaseExecutor { withDefaults = withoutClientMetadata; } + // 9router#1649: Mistral's API returns 422 (extra_forbidden) when an + // assistant message carries a `reasoning_content` field (replayed thinking + // from a prior turn, e.g. via the Codex /responses path). The field is + // nested per-message, so the generic top-level 400/field-downgrade retry + // doesn't cover it. Strip it from every message before sending — scoped to + // Mistral so DeepSeek (which *requires* replayed reasoning_content) is + // unaffected. + if ( + this.provider === "mistral" && + withDefaults && + typeof withDefaults === "object" && + !Array.isArray(withDefaults) && + Array.isArray((withDefaults as Record).messages) + ) { + const record = withDefaults as Record; + const messages = record.messages as unknown[]; + let mutated = false; + const cleaned = messages.map((msg) => { + if ( + msg && + typeof msg === "object" && + !Array.isArray(msg) && + Object.prototype.hasOwnProperty.call(msg, "reasoning_content") + ) { + mutated = true; + const { reasoning_content: _dropped, ...rest } = msg as Record; + return rest; + } + return msg; + }); + if (mutated) { + withDefaults = { ...record, messages: cleaned }; + } + } + const targetFormat = getTargetFormat(this.provider, credentials?.providerSpecificData); const requestFormat = withDefaults && typeof withDefaults === "object" && !Array.isArray(withDefaults) @@ -695,6 +737,23 @@ export class DefaultExecutor extends BaseExecutor { this.ensureThinkingBudget(withDefaults as Record, model); } + // 9router#1480: the native Moonshot `kimi` provider (executor "default") + // is a thinking-mode upstream that 400s with "reasoning_content must be + // passed back" when a prior assistant turn lacks it. OpencodeExecutor + // already injects a placeholder for OpenCode-routed thinking models; the + // direct kimi connection hit neither injection path. Scope to `kimi` so + // gateway-served models that merely match the thinking-model name pattern + // (and may reject an extra field) are unaffected. + if (this.provider === "kimi") { + const outboundModel = + typeof (withDefaults as Record)?.model === "string" + ? ((withDefaults as Record).model as string) + : model; + if (isThinkingMessageModel(outboundModel)) { + withDefaults = injectReasoningContentForThinkingModel(withDefaults); + } + } + return withDefaults; } diff --git a/open-sse/executors/doubao-web.ts b/open-sse/executors/doubao-web.ts index 10a9e395ea..c781834042 100644 --- a/open-sse/executors/doubao-web.ts +++ b/open-sse/executors/doubao-web.ts @@ -48,8 +48,11 @@ function parseJsonRecord(raw: string): JsonRecord | null { } function randomNumericId(length = 19): string { - let id = String(Math.floor(Math.random() * 9) + 1); - while (id.length < length) id += String(Math.floor(Math.random() * 10)); + // crypto-backed (CodeQL js/insecure-randomness): a synthetic device/web id, + // not a secret — but crypto.getRandomValues costs the same and closes the alert. + const bytes = globalThis.crypto.getRandomValues(new Uint8Array(length)); + let id = String((bytes[0] % 9) + 1); + for (let i = 1; i < length; i += 1) id += String(bytes[i] % 10); return id; } diff --git a/open-sse/executors/gitlab.ts b/open-sse/executors/gitlab.ts index 888e10b94a..594dfa7e47 100644 --- a/open-sse/executors/gitlab.ts +++ b/open-sse/executors/gitlab.ts @@ -27,13 +27,38 @@ import { type GitLabDirectAccessDetails, } from "@/lib/oauth/gitlab"; +type OpenAIToolCall = { + id?: string; + type?: string; + function?: { name?: string; arguments?: string }; +}; + type OpenAIMessage = { role?: string; content?: unknown; + tool_calls?: OpenAIToolCall[]; + tool_call_id?: string; + name?: string; }; type JsonRecord = Record; +/** + * Bounds for GitLab's `code_suggestions` `small_file` generation contract. Its AI-Gateway + * validation guard rejects an oversized/over-structured prompt with + * `422 {"detail":"Validation error"}` (tokens 0/0, pre-inference). Turn-1 is small and + * passes; a long folded tool-exchange history trips it — so the serialized prompt (and the + * `user_instruction` field) must stay bounded (#6220). + */ +const MAX_TOOL_EXCHANGE_CHARS = 24_000; +const MAX_TOOL_RESULT_CHARS = 8_000; +const MAX_USER_INSTRUCTION_CHARS = 4_000; + +function capText(text: string, max: number): string { + if (text.length <= max) return text; + return `${text.slice(0, max)}\n…[truncated ${text.length - max} chars]`; +} + type GitLabRequestTarget = { mode: "monolith" | "direct"; url: string; @@ -70,35 +95,158 @@ function extractTextContent(content: unknown): string { .trim(); } -function buildPrompt(messages: OpenAIMessage[] | undefined): string { - if (!Array.isArray(messages)) return ""; +/** + * GitLab code_suggestions is a single-prompt completion API (no chat roles), so the + * OpenAI message array must be flattened to text. + * + * Simple conversations keep the legacy shape (system instructions + latest user message). + * But when the array carries a **tool exchange** — an assistant with `tool_calls` or a + * `tool` result message — the full conversation is serialized instead, folding each tool + * result back keyed by its `tool_call_id`. Without this, `buildPrompt` dropped the + * assistant/tool turns and re-derived a byte-identical turn-1 prompt, so the model kept + * re-emitting the same `` call forever (#6220). Complements the tool_call emission + * added in #6051. + */ +function hasToolExchange(messages: OpenAIMessage[]): boolean { + return messages.some((message) => { + const role = String(message?.role || "user").toLowerCase(); + if (role === "tool") return true; + return ( + role === "assistant" && Array.isArray(message?.tool_calls) && message.tool_calls.length > 0 + ); + }); +} +/** Legacy flatten: system instructions + the latest user message. */ +function buildSimplePrompt(messages: OpenAIMessage[]): string { const systemParts: string[] = []; const userParts: string[] = []; - for (const message of messages) { const role = String(message?.role || "user").toLowerCase(); const text = extractTextContent(message?.content); if (!text) continue; - if (role === "system" || role === "developer") { systemParts.push(text); - continue; - } - - if (role === "user") { + } else if (role === "user") { userParts.push(text); } } - const latestUserPrompt = userParts.at(-1) || ""; - if (!systemParts.length) { - return latestUserPrompt; - } - + if (!systemParts.length) return latestUserPrompt; return `System instructions:\n${systemParts.join("\n\n")}\n\n${latestUserPrompt}`.trim(); } +/** Render an assistant turn (its text plus any tool calls) for the tool-exchange prompt. */ +function renderAssistantTurn(message: OpenAIMessage, text: string): string | null { + const lines: string[] = []; + if (text) lines.push(text); + for (const tc of Array.isArray(message?.tool_calls) ? message.tool_calls : []) { + const id = tc?.id ? ` [${tc.id}]` : ""; + lines.push( + `Called tool ${tc?.function?.name || "tool"}${id} with arguments: ${tc?.function?.arguments ?? ""}` + ); + } + return lines.length ? `Assistant: ${lines.join("\n")}` : null; +} + +/** Render one message as a labeled conversation line for the tool-exchange prompt. */ +function renderConversationTurn(message: OpenAIMessage, role: string, text: string): string | null { + if (role === "user") return text ? `User: ${text}` : null; + if (role === "assistant") return renderAssistantTurn(message, text); + if (role === "tool") { + const id = message?.tool_call_id ? ` for ${message.tool_call_id}` : ""; + const name = message?.name ? ` (${message.name})` : ""; + return `Tool result${name}${id}: ${text}`; + } + return null; +} + +/** + * Keep only what the `small_file` generation contract can carry: every system message, + * the latest user message, and the most-recent tool round (the last assistant `tool_calls` + * turn onward). Older turns are dropped so `content_above_cursor` stays bounded (#6220). + */ +function selectBoundedMessages(messages: OpenAIMessage[]): OpenAIMessage[] { + let lastUserIdx = -1; + let lastToolRoundIdx = -1; + messages.forEach((message, idx) => { + const role = String(message?.role || "user").toLowerCase(); + if (role === "user") lastUserIdx = idx; + if (role === "assistant" && Array.isArray(message?.tool_calls) && message.tool_calls.length) { + lastToolRoundIdx = idx; + } + }); + const tailStart = lastToolRoundIdx >= 0 ? lastToolRoundIdx : lastUserIdx; + const kept: OpenAIMessage[] = []; + messages.forEach((message, idx) => { + const role = String(message?.role || "user").toLowerCase(); + if (role === "system" || role === "developer") { + kept.push(message); + } else if (idx === lastUserIdx || idx >= tailStart) { + kept.push(message); + } + }); + return kept; +} + +/** + * Serialize the most-recent turn history so the model sees the tool result (#6220), but + * BOUNDED: only the last tool round is kept, each tool result is capped, and the whole + * prompt is capped — otherwise the folded history trips GitLab's `small_file` 422 guard. + */ +function buildToolExchangePrompt(messages: OpenAIMessage[]): string { + const systemParts: string[] = []; + const convo: string[] = []; + for (const message of selectBoundedMessages(messages)) { + const role = String(message?.role || "user").toLowerCase(); + let text = extractTextContent(message?.content); + if (role === "system" || role === "developer") { + if (text) systemParts.push(text); + continue; + } + if (role === "tool") text = capText(text, MAX_TOOL_RESULT_CHARS); + const line = renderConversationTurn(message, role, text); + if (line) convo.push(line); + } + const header = systemParts.length + ? `System instructions:\n${systemParts.join("\n\n")}\n\n` + : ""; + const body = `${header}${convo.join( + "\n\n" + )}\n\nContinue the response using the tool result above; do not repeat the tool call.`.trim(); + return capText(body, MAX_TOOL_EXCHANGE_CHARS); +} + +/** + * The user's actual instruction — the latest user message, capped. Kept separate from the + * folded history so it is NOT duplicated into an oversized `user_instruction`, the likely + * offending 422 field on tool-calling follow-up turns (#6220). + */ +function buildLatestUserInstruction(messages: OpenAIMessage[]): string { + for (let i = messages.length - 1; i >= 0; i--) { + const role = String(messages[i]?.role || "user").toLowerCase(); + if (role === "user") { + const text = extractTextContent(messages[i]?.content); + if (text) return capText(text, MAX_USER_INSTRUCTION_CHARS); + } + } + return ""; +} + +/** + * GitLab code_suggestions is a single-prompt completion API (no chat roles), so the + * OpenAI message array must be flattened to text. Simple conversations keep the legacy + * shape; a tool exchange (assistant `tool_calls` or a `tool` result) serializes the full + * conversation so the model continues instead of re-emitting the same `` call + * forever (#6220). Complements the tool_call emission added in #6051. + */ +export function buildPrompt(messages: OpenAIMessage[] | undefined): string { + if (!Array.isArray(messages)) return ""; + return hasToolExchange(messages) + ? buildToolExchangePrompt(messages) + : buildSimplePrompt(messages); +} + function toOpenAIError(status: number, message: string): Response { return new Response( JSON.stringify({ @@ -247,7 +395,16 @@ export class GitlabExecutor extends BaseExecutor { _stream: boolean, credentials: ExecuteInput["credentials"] ): Record { - const prompt = buildPrompt(body.messages as OpenAIMessage[] | undefined); + const messages = body.messages as OpenAIMessage[] | undefined; + const prompt = buildPrompt(messages); + // On a tool-exchange follow-up, `content_above_cursor` already carries the (bounded) + // folded history — sending the same long text again as `user_instruction` is the + // likely field that trips GitLab's `small_file` 422 guard. Send only the short latest + // user message there instead (#6220). Simple turns keep the legacy behavior. + const isToolExchange = Array.isArray(messages) && hasToolExchange(messages); + const userInstruction = isToolExchange + ? buildLatestUserInstruction(messages as OpenAIMessage[]) + : prompt; const providerData = credentials?.providerSpecificData && typeof credentials.providerSpecificData === "object" ? credentials.providerSpecificData @@ -272,7 +429,7 @@ export class GitlabExecutor extends BaseExecutor { generation_type: "small_file", stream: false, ...(projectPath ? { project_path: projectPath } : {}), - ...(prompt ? { user_instruction: prompt } : {}), + ...(userInstruction ? { user_instruction: userInstruction } : {}), }; } diff --git a/open-sse/executors/index.ts b/open-sse/executors/index.ts index 8da69d69b8..a922247c8c 100644 --- a/open-sse/executors/index.ts +++ b/open-sse/executors/index.ts @@ -42,6 +42,7 @@ import { T3ChatWebExecutor } from "./t3-chat-web.ts"; import { ClaudeWebExecutor } from "./claude-web.ts"; import { InnerAiExecutor } from "./inner-ai.ts"; import { HuggingChatExecutor } from "./huggingchat.ts"; +import { YuanbaoWebExecutor } from "./yuanbao-web.ts"; import { PoeWebExecutor } from "./poe-web.ts"; import { VeniceWebExecutor } from "./venice-web.ts"; import { V0VercelWebExecutor } from "./v0-vercel-web.ts"; @@ -131,6 +132,8 @@ const executors = { "in-ai": new InnerAiExecutor(), // Alias huggingchat: new HuggingChatExecutor(), hc: new HuggingChatExecutor(), // Alias + "yuanbao-web": new YuanbaoWebExecutor(), + ybw: new YuanbaoWebExecutor(), // Alias "poe-web": new PoeWebExecutor(), poe: new PoeWebExecutor(), // Alias "venice-web": new VeniceWebExecutor(), @@ -215,6 +218,7 @@ export { ClaudeWebExecutor } from "./claude-web.ts"; export { DeepSeekWebExecutor } from "./deepseek-web.ts"; export { DeepSeekWebWithAutoRefreshExecutor } from "./deepseek-web-with-auto-refresh.ts"; export { AdaptaWebExecutor } from "./adapta-web.ts"; +export { YuanbaoWebExecutor } from "./yuanbao-web.ts"; export { T3ChatWebExecutor } from "./t3-chat-web.ts"; export { InnerAiExecutor } from "./inner-ai.ts"; export { QwenWebExecutor } from "./qwen-web.ts"; diff --git a/open-sse/executors/opencode.ts b/open-sse/executors/opencode.ts index 60bde9d37c..60fe0f7951 100644 --- a/open-sse/executors/opencode.ts +++ b/open-sse/executors/opencode.ts @@ -248,9 +248,35 @@ export class OpencodeExecutor extends BaseExecutor { headers["Accept"] = "text/event-stream"; } - if (clientHeaders) { - forwardOpencodeClientHeaders(headers, clientHeaders, { + // Opt-in (#5997): synthesize OpenCode CLI identity headers the client did not send. + // Cloudflare in front of opencode.ai/zen/go 403s server-side (VPS) requests lacking + // CLI identity, but the forward-only default is deliberate — fabricating a WRONG + // value risks upstream rejection (#5720 regressed with "opencode/local"), and this + // is deployment-specific. So it stays OFF by default and the VPS operator enables it + // with OPENCODE_SYNTHESIZE_CLI_HEADERS=true (values env-overridable). Client-supplied + // headers always take precedence. + const synthesizeCli = /^(1|true|yes|on)$/i.test( + process.env.OPENCODE_SYNTHESIZE_CLI_HEADERS?.trim() ?? "" + ); + const cliDefaults = synthesizeCli + ? (() => { + const providerId = this.config?.id || this.provider || "opencode"; + const envUAKey = `${providerId.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_USER_AGENT`; + return { + userAgent: + process.env[envUAKey]?.trim() || + process.env.OPENCODE_USER_AGENT?.trim() || + "opencode-cli/1.0.0", + client: process.env.OPENCODE_CLIENT?.trim() || "cli", + project: process.env.OPENCODE_PROJECT?.trim() || "default", + }; + })() + : undefined; + + if (clientHeaders || cliDefaults) { + forwardOpencodeClientHeaders(headers, clientHeaders ?? {}, { synthesizeRequestId: true, + cliDefaults, }); } @@ -266,6 +292,19 @@ export class OpencodeExecutor extends BaseExecutor { credentials: ProviderCredentials ): any { let modifiedBody = super.transformRequest(model, body, stream, credentials); + // 9router#1442: OpenCode upstreams (e.g. kimi-k2.6 via opencode-go) return + // 400 "Extra inputs are not permitted, field: 'client_metadata'" — an + // OpenAI-Codex/Claude-CLI passthrough field with no equivalent here. The + // DefaultExecutor strip only covers cerebras/mistral, and OpencodeExecutor + // extends BaseExecutor directly, so nothing removed it on this path. + if ( + modifiedBody && + typeof modifiedBody === "object" && + !Array.isArray(modifiedBody) && + Object.prototype.hasOwnProperty.call(modifiedBody, "client_metadata") + ) { + delete (modifiedBody as Record).client_metadata; + } if ( modifiedBody && typeof modifiedBody === "object" && diff --git a/open-sse/executors/yuanbao-web.ts b/open-sse/executors/yuanbao-web.ts new file mode 100644 index 0000000000..20ef5662e0 --- /dev/null +++ b/open-sse/executors/yuanbao-web.ts @@ -0,0 +1,504 @@ +/** + * YuanbaoWebExecutor — Tencent Yuanbao (yuanbao.tencent.com) Web Provider + * + * Routes chat requests through the Tencent Yuanbao consumer web session. + * Requires the `hy_user` + `hy_token` cookies from a logged-in + * yuanbao.tencent.com browser session (paste the full Cookie header). + * + * API flow (verified against the reverse-engineered references below): + * 1. POST /api/user/agent/conversation/create { agentId } -> { id } (conversationId) + * 2. POST /api/chat/{conversationId} (JSON body) -> SSE stream + * + * Streaming format (SSE, `data: {json}` lines): + * - { type: "think", content: "..." } -- reasoning tokens (DeepSeek-R1 / Hunyuan-T1) + * - { type: "text", msg: "..." } -- answer tokens + * - { ..., stopReason: "..." } -- terminal marker + * + * References (endpoint/payload/session shape lifted + cross-checked): + * - juzeon/yuanbao-chat2api (Rust) — cookie-only auth: hy_user + hy_token + agentId + * - chenwr727/yuanbao-free-api (Python) — endpoints, body shape, model map + */ +import { + BaseExecutor, + mergeAbortSignals, + mergeUpstreamExtraHeaders, + type ExecuteInput, +} from "./base.ts"; +import { FETCH_TIMEOUT_MS } from "../config/constants.ts"; +import { buildErrorBody, sanitizeErrorMessage } from "../utils/error.ts"; +import { extractCookieValue, stripCookieInputPrefix } from "@/lib/providers/webCookieAuth"; + +const YUANBAO_BASE = "https://yuanbao.tencent.com"; +const CREATE_URL = `${YUANBAO_BASE}/api/user/agent/conversation/create`; +const CHAT_URL = `${YUANBAO_BASE}/api/chat`; + +// Public default DeepSeek agent id used by the Yuanbao web app. Not a secret — +// it is the shared consumer agent every logged-in session addresses by default. +const DEFAULT_AGENT_ID = "naQivTmsDa"; + +const USER_AGENT = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + + "(KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36"; + +const DEFAULT_MODEL = "deepseek-v3"; + +// OmniRoute model id -> Yuanbao internal chatModelId + optional supportFunctions. +const MODEL_MAP: Record = { + "deepseek-v3": { chatModelId: "deep_seek_v3" }, + "deepseek-r1": { chatModelId: "deep_seek" }, + "deepseek-v3-search": { + chatModelId: "deep_seek_v3", + supportFunctions: ["supportInternetSearch"], + }, + "deepseek-r1-search": { + chatModelId: "deep_seek", + supportFunctions: ["supportInternetSearch"], + }, + hunyuan: { chatModelId: "hunyuan_gpt_175B_0404" }, + "hunyuan-t1": { chatModelId: "hunyuan_t1" }, + "hunyuan-search": { + chatModelId: "hunyuan_gpt_175B_0404", + supportFunctions: ["supportInternetSearch"], + }, + "hunyuan-t1-search": { + chatModelId: "hunyuan_t1", + supportFunctions: ["supportInternetSearch"], + }, +}; + +// ── Helpers ───────────────────────────────────────────────────────────────── + +function isEncryptedCredentialBlob(value: unknown): boolean { + return typeof value === "string" && value.trim().startsWith("enc:v1:"); +} + +function extractText(content: unknown): string { + if (typeof content === "string") return content; + if (!Array.isArray(content)) return String(content ?? ""); + return content + .map((part: unknown) => { + if (!part || typeof part !== "object") return ""; + const item = part as Record; + if ((item.type === "text" || item.type === "input_text") && typeof item.text === "string") { + return item.text; + } + return ""; + }) + .filter((p: string) => p.length > 0) + .join("\n"); +} + +/** Flatten OpenAI messages into the single-prompt shape Yuanbao expects. */ +function buildPrompt(messages: Array>): string { + const parts: Array<{ role: string; content: string }> = []; + for (const msg of messages) { + const role = String(msg.role || "user"); + const text = extractText(msg.content).trim(); + if (!text) continue; + parts.push({ role, content: text }); + } + if (parts.length === 0) return ""; + if (parts.length === 1) return parts[0].content; + // Multi-turn: label each turn (matches the reference chat2api formatting). + return parts.map((p) => `#[${p.role.trim()}]\n${p.content}`).join("\n\n"); +} + +/** Build the `hy_source=web; hy_user=...; hy_token=...` cookie from the pasted header. */ +function buildYuanbaoCookie(rawApiKey: string): { cookie: string; hasToken: boolean } { + const raw = stripCookieInputPrefix(rawApiKey || ""); + const hyUser = extractCookieValue(raw, "hy_user"); + const hyToken = extractCookieValue(raw, "hy_token"); + + if (hyUser && hyToken) { + return { cookie: `hy_source=web; hy_user=${hyUser}; hy_token=${hyToken}`, hasToken: true }; + } + + // Fall back to forwarding whatever the user pasted (may already be a full + // Cookie header). Only usable if it plausibly carries the session token. + const hasToken = raw.includes("hy_token="); + return { cookie: raw, hasToken }; +} + +function estimateTokens(text: string): number { + return Math.max(1, Math.ceil((text || "").length / 4)); +} + +async function readUpstreamErrorDetails(response: Response): Promise<{ + message: string | null; + details: unknown; +}> { + const contentType = response.headers.get("content-type") || ""; + const text = await response.text().catch(() => ""); + if (!text) return { message: null, details: null }; + + if (contentType.includes("json")) { + try { + const parsed = JSON.parse(text) as Record; + const message = + typeof parsed.message === "string" + ? parsed.message + : typeof parsed.error === "string" + ? parsed.error + : null; + return { message: message ? sanitizeErrorMessage(message) : null, details: parsed }; + } catch { + // fall through + } + } + return { message: sanitizeErrorMessage(text), details: { body: text } }; +} + +// ── Executor ──────────────────────────────────────────────────────────────── + +export class YuanbaoWebExecutor extends BaseExecutor { + constructor() { + super("yuanbao-web", { id: "yuanbao-web", baseUrl: CHAT_URL }); + } + + private errorResponse(status: number, message: string, url: string, details?: unknown) { + return { + response: new Response(JSON.stringify(buildErrorBody(status, message, details)), { + status, + headers: { "Content-Type": "application/json" }, + }), + url, + headers: {}, + transformedBody: undefined, + }; + } + + async execute(input: ExecuteInput): Promise<{ + response: Response; + url: string; + headers: Record; + transformedBody: unknown; + }> { + const { model, body, stream, credentials, signal, log, upstreamExtraHeaders } = input; + const messages = (body as Record).messages as + | Array> + | undefined; + + if (!messages || !Array.isArray(messages) || messages.length === 0) { + return this.errorResponse(400, "Missing or empty messages array", CHAT_URL); + } + + if (isEncryptedCredentialBlob(credentials.apiKey)) { + return this.errorResponse( + 401, + "Yuanbao credentials are encrypted but STORAGE_ENCRYPTION_KEY is not loaded. " + + "Restore the encryption key or re-save the Yuanbao cookie.", + CREATE_URL + ); + } + + const { cookie, hasToken } = buildYuanbaoCookie(credentials.apiKey || ""); + if (!hasToken) { + return this.errorResponse( + 401, + "Yuanbao requires a session cookie. Log in to yuanbao.tencent.com, open " + + "DevTools > Application > Cookies, and paste the full Cookie header " + + "(it must contain hy_user and hy_token).", + CREATE_URL + ); + } + + const resolvedModel = model && MODEL_MAP[model] ? model : DEFAULT_MODEL; + const modelSpec = MODEL_MAP[resolvedModel]; + const prompt = buildPrompt(messages); + if (!prompt.trim()) { + return this.errorResponse(400, "Empty prompt after processing messages", CHAT_URL); + } + + const baseHeaders: Record = { + Cookie: cookie, + "User-Agent": USER_AGENT, + Origin: YUANBAO_BASE, + Referer: `${YUANBAO_BASE}/chat/${DEFAULT_AGENT_ID}`, + "X-Agentid": DEFAULT_AGENT_ID, + }; + + const timeoutSignal = AbortSignal.timeout(FETCH_TIMEOUT_MS); + const combinedSignal = signal ? mergeAbortSignals(signal, timeoutSignal) : timeoutSignal; + + // ── Step 1: create conversation ───────────────────────────────────────── + let conversationId: string; + try { + const createRes = await fetch(CREATE_URL, { + method: "POST", + headers: { ...baseHeaders, "Content-Type": "application/json" }, + body: JSON.stringify({ agentId: DEFAULT_AGENT_ID }), + signal: combinedSignal, + }); + + if (!createRes.ok) { + const status = createRes.status; + const upstreamError = await readUpstreamErrorDetails(createRes); + let message = `Yuanbao conversation creation failed (HTTP ${status})`; + if (status === 401 || status === 403) { + message = + "Yuanbao auth failed — your hy_user/hy_token cookies may be missing or expired. " + + "Log in to yuanbao.tencent.com and re-paste your Cookie header."; + } else if (status === 429) { + message = "Yuanbao rate limited. Wait a moment and retry."; + } + if (upstreamError.message) message = `${message}: ${upstreamError.message}`; + return this.errorResponse(status, message, CREATE_URL, upstreamError.details); + } + + const createData = (await createRes.json()) as Record; + conversationId = String(createData.id || ""); + if (!conversationId) { + return this.errorResponse( + 502, + "Yuanbao did not return a conversation id", + CREATE_URL + ); + } + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + log?.error?.("YUANBAO-WEB", `Conversation creation failed: ${message}`); + return this.errorResponse( + 502, + `Yuanbao connection failed: ${sanitizeErrorMessage(message)}`, + CREATE_URL + ); + } + + // ── Step 2: send message ──────────────────────────────────────────────── + const messageUrl = `${CHAT_URL}/${conversationId}`; + const chatBody: Record = { + model: "gpt_175B_0404", + prompt, + plugin: "Adaptive", + displayPrompt: prompt, + displayPromptType: 1, + options: { + imageIntention: { + needIntentionModel: true, + backendUpdateFlag: 2, + intentionStatus: true, + }, + }, + multimedia: [], + agentId: DEFAULT_AGENT_ID, + supportHint: 1, + version: "v2", + chatModelId: modelSpec.chatModelId, + }; + if (modelSpec.supportFunctions) chatBody.supportFunctions = modelSpec.supportFunctions; + + const chatHeaders: Record = { + ...baseHeaders, + "Content-Type": "application/json", + Accept: "text/event-stream", + }; + mergeUpstreamExtraHeaders(chatHeaders, upstreamExtraHeaders); + + let upstreamResponse: Response; + try { + upstreamResponse = await fetch(messageUrl, { + method: "POST", + headers: chatHeaders, + body: JSON.stringify(chatBody), + signal: combinedSignal, + }); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + log?.error?.("YUANBAO-WEB", `Message send failed: ${message}`); + return this.errorResponse( + 502, + `Yuanbao connection failed: ${sanitizeErrorMessage(message)}`, + messageUrl + ); + } + + if (!upstreamResponse.ok) { + const status = upstreamResponse.status; + const upstreamError = await readUpstreamErrorDetails(upstreamResponse); + let message = `Yuanbao returned HTTP ${status}`; + if (status === 401 || status === 403) { + message = "Yuanbao auth failed — session cookie may be expired."; + } else if (status === 429) { + message = "Yuanbao rate limited. Wait a moment and retry."; + } + if (upstreamError.message) message = `${message}: ${upstreamError.message}`; + return this.errorResponse(status, message, messageUrl, upstreamError.details); + } + + if (!upstreamResponse.body) { + return this.errorResponse(502, "Yuanbao returned empty response body", messageUrl); + } + + // ── Step 3: translate SSE → OpenAI ────────────────────────────────────── + const id = `chatcmpl-yuanbao-${crypto.randomUUID().slice(0, 12)}`; + const created = Math.floor(Date.now() / 1000); + + if (stream) { + return { + response: new Response( + transformYuanbaoStream(upstreamResponse.body, resolvedModel, id, created, signal, log), + { + status: 200, + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + "X-Accel-Buffering": "no", + }, + } + ), + url: messageUrl, + headers: chatHeaders, + transformedBody: chatBody, + }; + } + + const { content, reasoning } = await collectYuanbaoResponse(upstreamResponse.body, signal); + const completionTokens = estimateTokens(content + reasoning); + const messagePayload: Record = { role: "assistant", content }; + if (reasoning) messagePayload.reasoning_content = reasoning; + + return { + response: new Response( + JSON.stringify({ + id, + object: "chat.completion", + created, + model: resolvedModel, + choices: [{ index: 0, message: messagePayload, finish_reason: "stop" }], + usage: { + prompt_tokens: estimateTokens(prompt), + completion_tokens: completionTokens, + total_tokens: estimateTokens(prompt) + completionTokens, + }, + }), + { status: 200, headers: { "Content-Type": "application/json" } } + ), + url: messageUrl, + headers: chatHeaders, + transformedBody: chatBody, + }; + } +} + +// ── SSE translation helpers ─────────────────────────────────────────────────── + +interface YuanbaoEvent { + type?: string; + content?: string; + msg?: string; + stopReason?: string; +} + +function parseYuanbaoDataLine(line: string): YuanbaoEvent | null { + if (!line.startsWith("data: ")) return null; + const payload = line.slice(6).trim(); + if (!payload || payload === "[DONE]" || !payload.startsWith("{")) return null; + try { + return JSON.parse(payload) as YuanbaoEvent; + } catch { + return null; + } +} + +function transformYuanbaoStream( + upstream: ReadableStream, + model: string, + id: string, + created: number, + signal: AbortSignal | null | undefined, + log?: ExecuteInput["log"] +): ReadableStream { + const encoder = new TextEncoder(); + const decoder = new TextDecoder(); + let roleEmitted = false; + + return new ReadableStream({ + async start(controller) { + const reader = upstream.getReader(); + let buffer = ""; + + const emit = (delta: object, finish?: string | null) => { + controller.enqueue( + encoder.encode( + `data: ${JSON.stringify({ + id, + object: "chat.completion.chunk", + created, + model, + choices: [{ index: 0, delta, finish_reason: finish ?? null }], + })}\n\n` + ) + ); + }; + + const ensureRole = () => { + if (!roleEmitted) { + roleEmitted = true; + emit({ role: "assistant", content: "" }); + } + }; + + try { + while (true) { + if (signal?.aborted) break; + const { done, value } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split("\n"); + buffer = lines.pop() ?? ""; + for (const line of lines) { + const event = parseYuanbaoDataLine(line); + if (!event) continue; + if (event.type === "think" && event.content) { + ensureRole(); + emit({ reasoning_content: event.content }); + } else if (event.type === "text" && typeof event.msg === "string" && event.msg) { + ensureRole(); + emit({ content: event.msg }); + } + } + } + } catch (err) { + log?.error?.("YUANBAO-WEB", `Stream error: ${err}`); + } finally { + ensureRole(); + emit({}, "stop"); + controller.enqueue(encoder.encode("data: [DONE]\n\n")); + controller.close(); + reader.releaseLock(); + } + }, + }); +} + +async function collectYuanbaoResponse( + upstream: ReadableStream, + signal: AbortSignal | null | undefined +): Promise<{ content: string; reasoning: string }> { + const decoder = new TextDecoder(); + const reader = upstream.getReader(); + let buffer = ""; + let content = ""; + let reasoning = ""; + + try { + while (true) { + if (signal?.aborted) break; + const { done, value } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split("\n"); + buffer = lines.pop() ?? ""; + for (const line of lines) { + const event = parseYuanbaoDataLine(line); + if (!event) continue; + if (event.type === "think" && event.content) reasoning += event.content; + else if (event.type === "text" && typeof event.msg === "string") content += event.msg; + } + } + } finally { + reader.releaseLock(); + } + + return { content, reasoning }; +} diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index 33c33d9ffb..8d5bc29e04 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -374,6 +374,7 @@ export async function handleChatCore({ skipUpstreamRetry = false, createPiiTransform = null, correlationId = null, + modelPinned = false, }) { let { provider, model, extendedContext } = modelInfo; // ── Memory pressure guard ──────────────────────────────────────────── @@ -806,6 +807,7 @@ export async function handleChatCore({ apiKeyInfo, noLogEnabled, correlationId, + modelPinned, }); // Primary path: merge client model id + alias target so config on either key applies; resolved @@ -2105,7 +2107,15 @@ export async function handleChatCore({ // mode="cliproxyapi": returns the CLIProxyAPI executor instead. // mode="fallback": returns a wrapper that tries native first, falls back to CLIProxyAPI on 5xx/network errors. - const resolveExecutorWithProxy = (prov: string) => resolveExecutorWithProxyFor(prov, log); + // #6339: pass the resolved connection's providerSpecificData so a per-connection + // cliproxyapiMode="claude-native" override can deep-route this single connection + // through CLIProxyAPI regardless of the provider-level upstream_proxy_config mode. + const resolveExecutorWithProxy = (prov: string) => + resolveExecutorWithProxyFor( + prov, + log, + (credentials?.providerSpecificData as Record | null | undefined) ?? null + ); // === Quota Share enforcement PRE-hook (B/F7) === // Runs after provider/model/credentials/apiKeyInfo are fully resolved, @@ -4054,6 +4064,13 @@ export async function handleChatCore({ requestId: skillRequestId, compressionResponseMeta, }); + // #6426: align response body `model` with the `X-OmniRoute-Model` header + // (both must be the resolved backend model). Some upstreams (notably legacy + // /v1/completions text-completion path) return a body `model` field that + // differs from the resolved backend id we advertised in the header, leaving + // strict clients unable to reconcile the two. Rewrite body.model to `model` + // FIRST, then let #1311 echo override it when the opt-in setting is on. + if (typeof model === "string" && model) echoModelInObject(translatedResponse, model); // #1311: echo the requested alias/combo name in the non-streaming response model. if (echoModel) echoModelInObject(translatedResponse, echoModel); return { diff --git a/open-sse/handlers/chatCore/attemptLogging.ts b/open-sse/handlers/chatCore/attemptLogging.ts index 8c7151e567..9db3efbc74 100644 --- a/open-sse/handlers/chatCore/attemptLogging.ts +++ b/open-sse/handlers/chatCore/attemptLogging.ts @@ -51,6 +51,7 @@ export type PersistAttemptLogsContext = { apiKeyInfo: { id?: string | null; name?: string | null } | null | undefined; noLogEnabled: unknown; correlationId?: string | null; + modelPinned?: boolean; }; function toConnectionId(value: unknown): string | null { @@ -108,6 +109,7 @@ export function persistAttemptLogs(args: PersistAttemptLogsArgs, ctx: PersistAtt apiKeyInfo, noLogEnabled, correlationId, + modelPinned, } = ctx; const initialConnectionId = toConnectionId(connectionId); const finalConnectionId = toConnectionId(credentials?.connectionId) || initialConnectionId; @@ -203,5 +205,6 @@ export function persistAttemptLogs(args: PersistAttemptLogsArgs, ctx: PersistAtt noLog: noLogEnabled, pipelinePayloads, correlationId, + modelPinned: modelPinned || false, }).catch(() => {}); } diff --git a/open-sse/handlers/chatCore/executorProxy.ts b/open-sse/handlers/chatCore/executorProxy.ts index bb9e327ea9..4f0ea2a784 100644 --- a/open-sse/handlers/chatCore/executorProxy.ts +++ b/open-sse/handlers/chatCore/executorProxy.ts @@ -10,6 +10,7 @@ */ import { getExecutor } from "../../executors/index.ts"; +import { isCliproxyapiDeepModeEnabled } from "../../executors/cliproxyapi.ts"; import { getCachedSettings } from "@/lib/db/readCache"; import { getUpstreamProxyConfigCached } from "./comboContextCache.ts"; @@ -22,7 +23,25 @@ type LoggerLike = | null | undefined; -export async function resolveExecutorWithProxy(prov: string, log?: LoggerLike) { +export async function resolveExecutorWithProxy( + prov: string, + log?: LoggerLike, + providerSpecificData?: Record | null +) { + // Per-connection routing override (#6339): the resolved connection can opt itself + // into the CLIProxyAPI passthrough executor via providerSpecificData.cliproxyapiMode + // === "claude-native" (UI toggle). This takes precedence over the provider-level + // upstream_proxy_config mode — one connection can deep-route while the provider's + // default (and its other connections) stay native. Backward-compatible: connections + // without the flag fall through to the existing per-provider behaviour untouched. + if (isCliproxyapiDeepModeEnabled(providerSpecificData)) { + log?.info?.( + "UPSTREAM_PROXY", + `${prov} routed through CLIProxyAPI (per-connection claude-native override)` + ); + return getExecutor("cliproxyapi"); + } + const cfg = await getUpstreamProxyConfigCached(prov); if (!cfg.enabled || cfg.mode === "native") return getExecutor(prov); diff --git a/open-sse/handlers/responseSanitizer.ts b/open-sse/handlers/responseSanitizer.ts index 04d25871a6..25fcb0d616 100644 --- a/open-sse/handlers/responseSanitizer.ts +++ b/open-sse/handlers/responseSanitizer.ts @@ -68,6 +68,27 @@ function stripZeroWidthText(value: string): string { return value.replace(/[\u200B-\u200D\uFEFF]/g, ""); } +function stripZeroWidthToolArgumentJson(value: unknown): string { + return stripZeroWidthText(typeof value === "string" ? value : JSON.stringify(value || {})); +} + +function stripZeroWidthFunctionArguments(functionCall: unknown): unknown { + const fn = toRecord(functionCall); + if (!fn || typeof fn.arguments !== "string") return functionCall; + const stripped = stripZeroWidthText(fn.arguments); + // Fast path: return the original reference when there is nothing to strip, so + // hot streaming paths avoid a per-chunk shallow clone of every tool call. + if (stripped === fn.arguments) return functionCall; + return { ...fn, arguments: stripped }; +} + +function stripZeroWidthToolCallArguments(toolCall: unknown): unknown { + const tc = toRecord(toolCall); + if (!tc) return toolCall; + const fn = stripZeroWidthFunctionArguments(tc.function); + return fn === tc.function ? toolCall : { ...tc, function: fn }; +} + function stripZeroWidthValue(value: unknown): unknown { if (typeof value === "string") return stripZeroWidthText(value); if (Array.isArray(value)) return value.map((item) => stripZeroWidthValue(item)); @@ -418,11 +439,13 @@ function sanitizeMessage(msg: unknown, options: ParseOptions = {}): unknown { applyTextualToolCallSanitization(sanitized, msgRecord); if (msgRecord.tool_calls) { - sanitized.tool_calls = msgRecord.tool_calls; + sanitized.tool_calls = Array.isArray(msgRecord.tool_calls) + ? msgRecord.tool_calls.map((toolCall) => stripZeroWidthToolCallArguments(toolCall)) + : msgRecord.tool_calls; } if (msgRecord.function_call) { - sanitized.function_call = msgRecord.function_call; + sanitized.function_call = stripZeroWidthFunctionArguments(msgRecord.function_call); } return sanitized; @@ -554,6 +577,23 @@ function normalizeResponsesId(id: unknown): string { return `resp_${id}`; } +/** + * True when a Responses output item is an assistant `message` in the internal + * `commentary` phase — i.e. reasoning/scratchpad text that must never reach the + * client. Streaming `response.output_text.delta` events do not carry the `phase` + * themselves, so the passthrough path uses this on the `response.output_item.added` + * item to decide which subsequent deltas/dones to drop statefully (#6199). + */ +export function isResponsesCommentaryMessageItem(item: unknown): boolean { + const itemRecord = toRecord(item); + if (!itemRecord) return false; + const type = toString(itemRecord.type) || "message"; + if (type !== "message") return false; + const role = toString(itemRecord.role) || "assistant"; + const phase = toString(itemRecord.phase); + return role === "assistant" && phase === "commentary"; +} + function sanitizeResponsesStreamingOutputItem(item: unknown): JsonRecord | null { const itemRecord = toRecord(item); if (!itemRecord) return null; @@ -562,8 +602,7 @@ function sanitizeResponsesStreamingOutputItem(item: unknown): JsonRecord | null if (type === "message") { const role = toString(itemRecord.role) || "assistant"; - const phase = toString(itemRecord.phase); - if (role === "assistant" && phase === "commentary") { + if (isResponsesCommentaryMessageItem(itemRecord)) { return null; } @@ -611,10 +650,7 @@ function sanitizeResponsesStreamingOutputItem(item: unknown): JsonRecord | null return { ...itemRecord, type: "function_call", - arguments: - typeof itemRecord.arguments === "string" - ? itemRecord.arguments - : JSON.stringify(itemRecord.arguments || {}), + arguments: stripZeroWidthToolArgumentJson(itemRecord.arguments), }; } @@ -643,8 +679,8 @@ function sanitizeResponsesStreamingOutput(output: unknown): JsonRecord[] { // Native Responses streaming events that carry raw model text directly on the // root `delta` field. These must get the same zero-width-joiner stripping as the // non-streaming path so agent words (opencode/cursor/aider) are not corrupted. -// Scoped as an allow-list on purpose: `response.function_call_arguments.delta` -// carries tool-call argument JSON that must pass through byte-exact. +// Scoped as an allow-list on purpose: other root `delta` events may carry non-text payloads. +// Function-call argument events are handled separately by stripping only zero-width code points. const RESPONSES_STREAMING_TEXT_DELTA_EVENTS = new Set([ "response.output_text.delta", "response.reasoning_summary_text.delta", @@ -672,6 +708,18 @@ function sanitizeResponsesStreamingEvent(parsedRecord: JsonRecord): JsonRecord { if (RESPONSES_STREAMING_TEXT_DONE_EVENTS.has(eventType) && typeof sanitized.text === "string") { sanitized.text = stripZeroWidthText(sanitized.text); } + if ( + eventType === "response.function_call_arguments.delta" && + typeof sanitized.delta === "string" + ) { + sanitized.delta = stripZeroWidthText(sanitized.delta); + } + if ( + eventType === "response.function_call_arguments.done" && + typeof sanitized.arguments === "string" + ) { + sanitized.arguments = stripZeroWidthText(sanitized.arguments); + } if (parsedRecord.item !== undefined) { const sanitizedItem = sanitizeResponsesStreamingOutputItem(parsedRecord.item); @@ -770,10 +818,7 @@ function sanitizeResponsesOutputItem(item: unknown, index: number): JsonRecord | type: "function_call", call_id: callId, name: toString(itemRecord.name) || "", - arguments: - typeof itemRecord.arguments === "string" - ? itemRecord.arguments - : JSON.stringify(itemRecord.arguments || {}), + arguments: stripZeroWidthToolArgumentJson(itemRecord.arguments), }; } @@ -915,8 +960,7 @@ function convertOpenAIResponseToResponses(openaiResponse: JsonRecord): JsonRecor type: "function_call", call_id: callId, name: toString(fn.name) || "", - arguments: - typeof fn.arguments === "string" ? fn.arguments : JSON.stringify(fn.arguments || {}), + arguments: stripZeroWidthToolArgumentJson(fn.arguments), }); } @@ -1003,15 +1047,17 @@ export function sanitizeStreamingChunk(parsed: unknown): unknown { ? deltaRecord.tool_calls.map((tc) => { const t = toRecord(tc); if (!t) return tc; + const strippedToolCall = stripZeroWidthToolCallArguments(t); + const strippedRecord = toRecord(strippedToolCall) || t; if (t.id !== undefined && t.id !== null && typeof t.id !== "string") { - return { ...t, id: String(t.id) }; + return { ...strippedRecord, id: String(t.id) }; } - return t; + return strippedRecord; }) : deltaRecord.tool_calls; } if (deltaRecord.function_call !== undefined) - delta.function_call = deltaRecord.function_call; + delta.function_call = stripZeroWidthFunctionArguments(deltaRecord.function_call); c.delta = delta; } else { c.delta = choiceRecord.delta; diff --git a/open-sse/mcp-server/server.ts b/open-sse/mcp-server/server.ts index 718e226a33..77ee0706b8 100644 --- a/open-sse/mcp-server/server.ts +++ b/open-sse/mcp-server/server.ts @@ -67,6 +67,7 @@ import { handlePickFastestModel } from "./tools/pickFastestModel.ts"; import { memoryTools } from "./tools/memoryTools.ts"; import { skillTools } from "./tools/skillTools.ts"; import { agentSkillTools } from "./tools/agentSkillTools.ts"; +import { githubSkillTools } from "./tools/githubSkillTools.ts"; import { skillRegistry } from "../../src/lib/skills/registry.ts"; import { skillExecutor } from "../../src/lib/skills/executor.ts"; import { pluginTools } from "./tools/pluginTools.ts"; @@ -102,6 +103,7 @@ const TOTAL_MCP_TOOL_COUNT = Object.keys(memoryTools).length + Object.keys(skillTools).length + Object.keys(agentSkillTools).length + + Object.keys(githubSkillTools).length + Object.keys(poolTools).length + gamificationTools.length + pluginTools.length + @@ -1062,6 +1064,29 @@ export function createMcpServer(): McpServer { ); }); + // ── GitHub Skill Tools ────────────────────────── + Object.values(githubSkillTools).forEach((toolDef) => { + server.registerTool( + toolDef.name, + { + description: toolDef.description, + // @ts-ignore: dynamic zod access + inputSchema: toolDef.inputSchema, + }, + withScopeEnforcement(toolDef.name, async (args) => { + try { + const parsedArgs = toolDef.inputSchema.parse(args ?? {}); + // @ts-expect-error - handler type lost through dynamic Object.values() access + const result = await toolDef.handler(parsedArgs); + return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] }; + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true }; + } + }, toolDef.scopes) + ); + }); + // ── Plugin Tools ────────────────────────────── pluginTools.forEach((toolDef) => { server.registerTool( diff --git a/open-sse/mcp-server/tools/githubSkillTools.ts b/open-sse/mcp-server/tools/githubSkillTools.ts new file mode 100644 index 0000000000..2161ec0945 --- /dev/null +++ b/open-sse/mcp-server/tools/githubSkillTools.ts @@ -0,0 +1,141 @@ +/** + * githubSkillTools.ts — MCP tools for GitHub agent skill discovery and import. + * + * Provides tools to: + * - Search GitHub for repos with SKILL.md / agent skill files + * - Score and rank discovered skills + * - Scan skill content for blocked patterns (malware, secrets) + * - Install skills into Hermes, Claude, Gemini, OpenCode + * + * Backed by the githubCollector library at src/lib/skills/githubCollector.ts. + */ + +import { z } from "zod"; +import { sanitizeErrorMessage } from "../../utils/error.ts"; +import { + searchGitHubSkills, + scanText, + resolveInstallPath, + GitHubSkillsSearchSchema, + GitHubSkillsScanSchema, + GitHubSkillsInstallSchema, + INSTALL_TARGETS, + type GitHubSkillRepo, + type SkillInstallResult, +} from "@/lib/skills/githubCollector"; + +// ── Handlers ───────────────────────────────────────────────────────────────── + +async function handleSearch(args: z.infer) { + const { repos, errors } = await searchGitHubSkills({ + minStars: args.minStars, + maxResults: args.maxResults, + }); + + let filtered = repos; + if (args.minScore > 0) filtered = filtered.filter((r) => r.score >= args.minScore); + if (args.query) { + const q = args.query.toLowerCase(); + filtered = filtered.filter( + (r) => r.fullName.toLowerCase().includes(q) || r.description.toLowerCase().includes(q) + ); + } + + return { + skills: filtered.map((r: GitHubSkillRepo) => ({ + fullName: r.fullName, + stars: r.stars, + score: r.score, + description: r.description.slice(0, 200), + topics: r.topics, + htmlUrl: r.htmlUrl, + hasSkillFile: r.hasSkillFile, + license: r.license, + })), + total: filtered.length, + errors: errors.length > 0 ? errors : undefined, + }; +} + +async function handleScan(args: z.infer) { + const findings = scanText(args.content, args.repoName); + return { + repoName: args.repoName, + clean: findings.length === 0, + findings: findings.map((f) => ({ + pattern: f.pattern, + context: f.context, + })), + }; +} + +async function handleInstall(args: z.infer) { + const results: SkillInstallResult[] = []; + const skillName = args.repoName.split("/").pop() || args.repoName; + + for (const target of args.targets) { + try { + const dest = resolveInstallPath(target, skillName, args.description); + // In a real implementation, this would clone the repo and copy files. + // For now, we return the planned install path as a dry-run result. + results.push({ + target, + ok: true, + action: "installed", + destDir: dest, + }); + } catch (err) { + results.push({ + target, + ok: false, + action: "error", + error: sanitizeErrorMessage((err as Error).message), + }); + } + } + + return { + repoName: args.repoName, + skillName, + results, + allOk: results.every((r) => r.ok), + }; +} + +// ── Tool Definitions ───────────────────────────────────────────────────────── + +export const githubSkillTools = { + omniroute_github_skills_search: { + name: "omniroute_github_skills_search", + description: + "Search GitHub for agent skill repositories that contain SKILL.md, CLAUDE.md, .cursorrules, or similar agent configuration files. " + + "Returns scored results sorted by relevance. Scores are 0.0–1.0 based on stars, name signals, description keywords, and topic tags. " + + "Ideal for discovering community agent skills from GitHub.", + inputSchema: GitHubSkillsSearchSchema, + scopes: ["read:skills"], + handler: handleSearch, + }, + + omniroute_github_skills_scan: { + name: "omniroute_github_skills_scan", + description: + "Scan SKILL.md or README content from a GitHub repo for blocked patterns including eval(base64), " + + "hardcoded secrets (API keys, passwords, private keys), dangerous function calls (os.system, subprocess.Popen), " + + "and other malware indicators. Returns findings with context or 'clean' status.", + inputSchema: GitHubSkillsScanSchema, + scopes: ["read:skills"], + handler: handleScan, + }, + + omniroute_github_skills_install: { + name: "omniroute_github_skills_install", + description: + "Preview or plan the installation of a discovered GitHub skill into one or more agent directories " + + "(Hermes: ~/AppData/Local/hermes/skills/, Claude: ~/.claude/skills/, Gemini: ~/.gemini/skills/, " + + "OpenCode: ~/.opencode/skills/). Categorizes the skill based on its name and description. " + + "Returns the target paths where the skill would be installed.", + inputSchema: GitHubSkillsInstallSchema, + scopes: ["read:skills", "write:skills"], + handler: handleInstall, + }, +}; diff --git a/open-sse/package.json b/open-sse/package.json index 05b24a10a5..f1a7c838e6 100644 --- a/open-sse/package.json +++ b/open-sse/package.json @@ -1,6 +1,6 @@ { "name": "@omniroute/open-sse", - "version": "3.8.45", + "version": "3.8.46", "description": "Express SSE sidecar for OmniRoute — handles streaming, protocol translation, and provider orchestration", "type": "module", "main": "index.js", diff --git a/open-sse/services/ccWireImageBuiltins.ts b/open-sse/services/ccWireImageBuiltins.ts new file mode 100644 index 0000000000..870e4f78f1 --- /dev/null +++ b/open-sse/services/ccWireImageBuiltins.ts @@ -0,0 +1,26 @@ +/** + * Built-in provider ids that must adopt the dynamic Claude-Code wire image + * (fingerprint headers/order + system transforms + `?beta=true` chat path) + * WITHOUT inheriting the Claude-Code-Compatible family's default anthropic + * baseUrl / Bearer auth. + * + * These providers keep their own registry `baseUrl` and auth scheme + * (e.g. `agentrouter` → `https://agentrouter.org/v1/messages` + `x-api-key`), + * while the two CC predicates (`isClaudeCodeCompatible` / + * `isClaudeCodeCompatibleProvider`) and `applyFingerprint` treat them as CC + * for the wire-image concerns only. The CC-baseUrl / CC-Bearer branches in + * `buildProviderUrl` / `buildProviderHeaders` are guarded so the registry + * baseUrl + auth are preserved. + * + * Single source of truth — imported by both predicates so they never diverge. + * See issue #6056. + */ +export const CC_WIRE_IMAGE_BUILTINS: ReadonlySet = new Set(["agentrouter"]); + +/** + * True when `provider` is a built-in that adopts the dynamic Claude-Code wire + * image while keeping its own registry baseUrl + auth. + */ +export function usesCcWireImage(provider: unknown): boolean { + return typeof provider === "string" && CC_WIRE_IMAGE_BUILTINS.has(provider); +} diff --git a/open-sse/services/claudeCodeCompatible.ts b/open-sse/services/claudeCodeCompatible.ts index 4fd5f22711..eff67c4203 100644 --- a/open-sse/services/claudeCodeCompatible.ts +++ b/open-sse/services/claudeCodeCompatible.ts @@ -15,6 +15,7 @@ import { import { applyClaudeCodeCompatibleThinkingDisplay } from "./claudeCodeCompatibleThinkingDisplay.ts"; import { obfuscateInBody } from "./claudeCodeObfuscation.ts"; import { applySystemTransformPipeline, PROVIDER_CC_BRIDGE } from "./systemTransforms.ts"; +import { usesCcWireImage } from "./ccWireImageBuiltins.ts"; import { fixToolPairs, fixToolAdjacency, @@ -95,7 +96,12 @@ function supportsClaudeXHighEffort(model: string | null | undefined): boolean { } export function isClaudeCodeCompatibleProvider(provider: string | null | undefined): boolean { - return typeof provider === "string" && provider.startsWith(CLAUDE_CODE_COMPATIBLE_PREFIX); + return ( + (typeof provider === "string" && provider.startsWith(CLAUDE_CODE_COMPATIBLE_PREFIX)) || + // Built-in providers (e.g. agentrouter) that adopt the dynamic CC wire image + // while keeping their own registry baseUrl + auth (#6056). + usesCcWireImage(provider) + ); } export function stripAnthropicMessagesSuffix(baseUrl: string | null | undefined): string { diff --git a/open-sse/services/combo.ts b/open-sse/services/combo.ts index a86bf5d90f..3b7c274061 100644 --- a/open-sse/services/combo.ts +++ b/open-sse/services/combo.ts @@ -12,6 +12,7 @@ import { formatRetryAfter, getModelLockoutInfo, getRuntimeProviderProfile, + hasPerModelQuota, isModelLocked, recordModelLockoutFailure, recordProviderFailure, @@ -58,7 +59,11 @@ import { handlePipelineCombo, buildPipelineResponse } from "./autoCombo/pipeline import { type ProviderCandidate } from "./autoCombo/scoring.ts"; import { estimateTokens } from "./contextManager.ts"; import { getSessionConnection } from "./sessionManager.ts"; -import { applySessionStickiness, recordStickyBinding } from "./combo/sessionStickiness.ts"; +import { + applySessionStickiness, + recordStickyBinding, + resolveDisableSessionStickiness, +} from "./combo/sessionStickiness.ts"; import { selectQuotaShareTarget } from "./combo/quotaShareStrategy.ts"; import { makeConnectionConcurrencyResolver, lookupPositiveCap } from "./combo/concurrencyCaps.ts"; import { acquireQuotaShareConcurrencySlot } from "./combo/quotaShareConcurrency.ts"; @@ -103,13 +108,18 @@ import { getStickyWeightedExecutionKey, recordStickyWeightedSuccess, } from "./combo/rrState.ts"; -import { validateResponseQuality, toRetryAfterDisplayValue } from "./combo/validateQuality.ts"; +import { + validateResponseQuality, + releaseQualityClone, + toRetryAfterDisplayValue, +} from "./combo/validateQuality.ts"; import { resolveComboCooldownWaitDecision } from "./combo/comboCooldownRetry.ts"; import { computeClosestRetryAfter, waitForCooldownAwareRetry, } from "../../src/sse/services/cooldownAwareRetry.ts"; import { handleFusionChat, type FusionTuning } from "./fusion.ts"; +import { handlePipelineChat, type PipelineStep } from "./pipeline.ts"; import { TRANSIENT_FOR_SEMAPHORE, MAX_FALLBACK_WAIT_MS, @@ -133,6 +143,7 @@ import { expandProviderWildcardsInCollection, } from "./combo/providerWildcard.ts"; import { resolveShadowTargets, scheduleShadowRouting } from "./combo/shadowRouting.ts"; +import { attemptCompatRejectedFallback } from "./combo/comboCompatFallback.ts"; import { filterTargetsByRequestCompatibility, resolveComboRuntimeUnits, @@ -701,7 +712,50 @@ export async function handleComboChat({ "COMBO", `Bypassing strategy — routing directly to pinned context model: ${pinnedModel}` ); - return handleSingleModelWithTimeout(body, pinnedModel); + let pinnedResult: Response | null = null; + try { + pinnedResult = await handleSingleModelWithTimeout(body, pinnedModel, { + modelPinned: true, + } as SingleModelTarget); + } catch (pinErr) { + log.warn( + "COMBO", + `Pinned model ${pinnedModel} threw error: ${pinErr instanceof Error ? pinErr.message : String(pinErr)}, falling through to combo retry/fallback` + ); + } + if (pinnedResult) { + if (pinnedResult.ok) { + let pinnedClone: Response; + try { + pinnedClone = pinnedResult.clone(); + } catch { + pinnedClone = pinnedResult; + } + const pinnedQuality = await validateResponseQuality( + pinnedClone, + clientRequestedStream, + log, + config.responseValidation + ); + releaseQualityClone(pinnedClone, pinnedResult, pinnedQuality); + if (pinnedQuality.valid) return pinnedResult; + log.warn( + "COMBO", + `Pinned model ${pinnedModel} returned 200 but failed quality check: ${pinnedQuality.reason}, falling through to combo retry/fallback` + ); + } else { + const pinnedStatus = pinnedResult.status || 500; + if (![408, 429, 500, 502, 503, 504].includes(pinnedStatus)) { + return pinnedResult; + } + log.warn( + "COMBO", + `Pinned model ${pinnedModel} failed (${pinnedStatus}), falling through to combo retry/fallback` + ); + } + } + // Fall through to the target iteration loop below — retries and sibling + // models will be tried via the normal combo machinery. } log.warn( "COMBO", @@ -709,7 +763,8 @@ export async function handleComboChat({ ? `Context-cache pin "${pinnedModel}" provider durably unhealthy — dropping pin, using strategy` : `Stale context-cache pin "${pinnedModel}" not in combo "${combo.name}" targets — dropping pin, using strategy` ); - return handleSingleModelWithTimeout(body, pinnedModel); + // Fall through to the normal target iteration loop below — the pin is + // dropped, so the combo strategy picks the best available target. } // Fusion strategy: parallel panel + judge synthesis. Handled in a separate module @@ -743,6 +798,37 @@ export async function handleComboChat({ }); } + // Pipeline strategy: sequential chain — each step's output feeds the next step's + // input, only the final step's response is returned. Handled in a separate module + // because it neither iterates targets as fallbacks nor needs the failover/retry + // machinery below — it runs targets in order, threading output → input. The step + // list is `combo.models` (in order); an optional per-step `prompt` is read off the + // target object (comboModelStepInputSchema.prompt). + if (strategy === "pipeline") { + const pipelineSteps = (combo.models || []) + .map((m): PipelineStep | null => { + if (typeof m === "string") return { model: m }; + if (m && typeof m === "object") { + const obj = m as Record; + if (typeof obj.model === "string") { + return { + model: obj.model, + prompt: typeof obj.prompt === "string" ? obj.prompt : undefined, + }; + } + } + return null; + }) + .filter((s): s is PipelineStep => Boolean(s)); + return handlePipelineChat({ + body, + steps: pipelineSteps, + handleSingleModel: handleSingleModelWithTimeout, + log, + comboName: combo.name, + }); + } + const nestingContext = nesting || { depth: 0, maxDepth: clampComboDepth(config.maxComboDepth), @@ -1069,10 +1155,20 @@ export async function handleComboChat({ apiKeyAllowedConnections, }); } - const _sticky = await applySessionStickiness( - orderedTargets, - body.messages as Array<{ role?: string; content?: unknown }> + // #6168: session stickiness opt-out. Per-combo `config.disableSessionStickiness` + // overrides the global `settings.disableSessionStickiness` fallback (default false, + // preserving the #3825 prompt-cache/504 fix). When disabled, skip the reorder and + // treat the result as a no-op so the recordStickyBinding write-back below is skipped. + const disableSessionStickiness = resolveDisableSessionStickiness( + config as Record | null | undefined, + settings as Record | null | undefined ); + const _sticky = disableSessionStickiness + ? ({ targets: orderedTargets, messageHash: null, stuck: false } as const) + : await applySessionStickiness( + orderedTargets, + body.messages as Array<{ role?: string; content?: unknown }> + ); orderedTargets = _sticky.targets; orderedTargets = orderTargetsByEvalScores(orderedTargets, config.evalRouting, log); orderedTargets = filterTargetsByRequestCompatibility(orderedTargets, body, log); @@ -1490,12 +1586,22 @@ export async function handleComboChat({ undefined; const effectiveConnectionId = selectedConnectionId || target.connectionId || ""; + // Clone BEFORE quality check — validateResponseQuality reads the body + // via getReader() which locks the stream. The clone's body is consumed + // by the quality check; the original stays unlocked for piping. + let qualityClone: Response; + try { + qualityClone = result.clone(); + } catch { + qualityClone = result; + } const quality = await validateResponseQuality( - result, + qualityClone, clientRequestedStream, log, config.responseValidation ); + releaseQualityClone(qualityClone, result, quality); if (!quality.valid) { log.warn( "COMBO", @@ -1730,7 +1836,7 @@ export async function handleComboChat({ })(); } - return { ok: true, response: quality.clonedResponse ?? result }; + return { ok: true, response: result }; } // Extract error info from response @@ -2009,7 +2115,16 @@ export async function handleComboChat({ } log.warn("COMBO", `Model ${modelStr} failed, trying next`, { status: result.status }); - if (resilienceSettings.providerCooldown.enabled && provider && provider !== "unknown") { + // #5976: per-model-quota providers (Gemini, GitHub, etc.) multiplex models + // behind one connection. A model-level 500 must NOT cool down the entire + // provider — sibling models may still succeed. Skip cooldown recording for + // these providers on 500 errors so the next target can try. + if ( + resilienceSettings.providerCooldown.enabled && + provider && + provider !== "unknown" && + !(result.status === 500 && hasPerModelQuota(provider, rawModel)) + ) { recordProviderCooldown( provider, targetWithConnection.connectionId ?? undefined, @@ -2289,6 +2404,15 @@ async function handleRoundRobinCombo({ log, "Context-aware round-robin fallback" ); + // #6238: keep the targets the compat pre-filter rejected so they can serve as a + // last-resort fallback tier. The pre-filter drops request-incompatible targets + // BEFORE availability is known; if every compat-kept target then turns out to be + // runtime-unavailable, we must reconsider these before returning 503, instead of + // permanently dropping a compat-rejected-but-healthy provider. + const compatKeptSet = new Set(filteredTargets); + const compatRejectedTargets = evalRankedTargets.filter( + (target) => !compatKeptSet.has(target) + ); const modelCount = filteredTargets.length; if (modelCount === 0) { return comboModelNotFoundResponse("Round-robin combo has no executable targets"); @@ -2389,10 +2513,19 @@ async function handleRoundRobinCombo({ // call — so sessionless RR combos rotated every turn, busting the upstream prompt-cache. // Reuse the SAME mechanism: start the rotation at the conversation's sticky connection // (the loop still falls through to the other targets on failure → failover preserved). - const _rrSessionSticky = await applySessionStickiness( - filteredTargets, - body?.messages as Array<{ role?: string; content?: unknown }> + // #6168: honor the session-stickiness opt-out here too, otherwise round-robin would + // still pin the conversation even when the flag is set. Per-combo `config` overrides + // the global `settings.disableSessionStickiness` fallback (default false). + const disableSessionStickiness = resolveDisableSessionStickiness( + config as Record | null | undefined, + settings as Record | null | undefined ); + const _rrSessionSticky = disableSessionStickiness + ? ({ targets: filteredTargets, messageHash: null, stuck: false } as const) + : await applySessionStickiness( + filteredTargets, + body?.messages as Array<{ role?: string; content?: unknown }> + ); let rrStartIndex = startIndex; if (_rrSessionSticky.stuck) { const stickyIdx = filteredTargets.findIndex( @@ -2549,12 +2682,19 @@ async function handleRoundRobinCombo({ // Success — validate response quality before returning if (result.ok) { + let rrClone: Response; + try { + rrClone = result.clone(); + } catch { + rrClone = result; + } const quality = await validateResponseQuality( - result, + rrClone, clientRequestedStream, log, config.responseValidation ); + releaseQualityClone(rrClone, result, quality); if (!quality.valid) { log.warn( "COMBO-RR", @@ -2614,6 +2754,15 @@ async function handleRoundRobinCombo({ if (stickyRoundRobinEnabled) { recordStickyRoundRobinSuccess(combo.name, target, stickyLimit, filteredTargets); + } else { + // #948: true round-robin (stickyLimit <= 1). The counter was advanced + // eagerly (+1 from the scheduled start index) before this loop ran, so + // when the scheduled model failed and a *different* model served via + // fallback, the next request reused the fallback-served model. Advance + // the pointer past the model that ACTUALLY served (modelIndex) instead, + // mirroring recordStickyRoundRobinSuccess's served-index logic. Read + // side applies `% modelCount`, so storing modelIndex + 1 is correct. + rrCounters.set(combo.name, modelIndex + 1); } // #3825: (re)record the sticky binding so the next turn re-pins (prompt-cache). @@ -2642,12 +2791,8 @@ async function handleRoundRobinCombo({ } })(); } - // validateResponseQuality peeks streaming bodies via getReader(), - // which locks `result.body`. It returns a clonedResponse that replays - // the buffered prefix and forwards the rest. Returning the original - // (now-locked) `result` makes Next.js throw "ReadableStream is locked" - // → 500. Mirror the priority strategy and return the replay response. - return quality.clonedResponse ?? result; + // Clone is consumed by quality check; original stays unlocked. + return result; } // Extract error info @@ -2824,7 +2969,12 @@ async function handleRoundRobinCombo({ if (offset > 0) fallbackCount++; log.warn("COMBO-RR", `${modelStr} failed, trying next model`, { status: result.status }); - if (resilienceSettings.providerCooldown.enabled && provider && provider !== "unknown") { + if ( + resilienceSettings.providerCooldown.enabled && + provider && + provider !== "unknown" && + !(result.status === 500 && hasPerModelQuota(provider, parseModel(modelStr).model || modelStr)) + ) { recordProviderCooldown( provider, targetWithConnection.connectionId ?? undefined, @@ -2865,6 +3015,37 @@ async function handleRoundRobinCombo({ // All models exhausted const latencyMs = Date.now() - startTime; + + // #6238: every compat-kept target was skipped as unavailable and NONE was ever + // attempted (recordedAttempts === 0). Before crystallizing 503, probe the targets + // the compat pre-filter rejected — a compat-rejected-but-healthy provider is a + // valid last-resort fallback tier, not a permanently dropped target. + if (recordedAttempts === 0 && compatRejectedTargets.length > 0) { + const compatFallbackResult = await attemptCompatRejectedFallback(compatRejectedTargets, body, { + handleSingleModel, + isModelAvailable, + isProviderInCooldown: (target) => + resilienceSettings.providerCooldown.enabled && + Boolean(target.provider && target.provider !== "unknown") && + isProviderInCooldown( + target.provider as string, + target.connectionId as string | undefined, + resilienceSettings + ), + log, + strategy: "round-robin", + }); + if (compatFallbackResult) { + recordComboRequest(combo.name, null, { + success: true, + latencyMs: Date.now() - startTime, + fallbackCount, + strategy: "round-robin", + }); + return compatFallbackResult; + } + } + if (recordedAttempts === 0) { recordComboRequest(combo.name, null, { success: false, diff --git a/open-sse/services/combo/comboCompatFallback.ts b/open-sse/services/combo/comboCompatFallback.ts new file mode 100644 index 0000000000..127f2c354c --- /dev/null +++ b/open-sse/services/combo/comboCompatFallback.ts @@ -0,0 +1,82 @@ +import type { ComboLogger, HandleSingleModel, IsModelAvailable, ResolvedComboTarget } from "./types"; + +/** + * Last-resort fallback tier for combo routing (#6238). + * + * `filterTargetsByRequestCompatibility` drops targets that look request-incompatible + * (tool/vision/structured-output unsupported, or below the required context window) + * BEFORE any runtime availability check runs. Its only safety net triggers when ALL + * targets are filtered — not when the kept targets are later all runtime-unavailable + * (circuit-open / cooldown / no credentials). In that case a combo would return + * `503 ALL_ACCOUNTS_INACTIVE` without ever reconsidering a compat-rejected-but-healthy + * target. + * + * This helper makes those compat-rejected targets a genuine fallback tier: only after + * the primary (compat-kept) targets were all skipped without a single real attempt do + * we probe the rejected set. A compat-rejected target is used only if it is actually + * available and returns a successful upstream response; otherwise the caller keeps its + * original error surface unchanged. + */ +export interface CompatFallbackContext { + handleSingleModel: HandleSingleModel; + isModelAvailable?: IsModelAvailable; + /** Returns true when the target's provider connection is in resilience cooldown. */ + isProviderInCooldown?: (target: ResolvedComboTarget) => boolean; + log: ComboLogger; + /** Effective combo strategy, threaded into the single-model dispatch for telemetry. */ + strategy: string; +} + +/** + * Attempt each compatibility-rejected target in order, returning the first successful + * upstream `Response`. Targets that are unavailable, in cooldown, or return a non-ok + * response are skipped. Returns `null` when no rejected target yields a success, so the + * caller can fall through to its existing error/503 path with an unchanged error surface. + */ +export async function attemptCompatRejectedFallback( + rejectedTargets: ResolvedComboTarget[], + body: Record, + ctx: CompatFallbackContext +): Promise { + if (rejectedTargets.length === 0) return null; + + for (const target of rejectedTargets) { + if (ctx.isModelAvailable) { + const available = await ctx.isModelAvailable(target.modelStr, target); + if (!available) { + ctx.log.debug( + "COMBO", + `Last-resort compat fallback: ${target.modelStr} still unavailable — skipping` + ); + continue; + } + } + + if (ctx.isProviderInCooldown?.(target)) { + ctx.log.debug( + "COMBO", + `Last-resort compat fallback: ${target.modelStr} provider in cooldown — skipping` + ); + continue; + } + + ctx.log.info( + "COMBO", + `Last-resort compat fallback → ${target.modelStr} (all compat-kept targets were unavailable)` + ); + const result = await ctx.handleSingleModel(body, target.modelStr, { + ...target, + effectiveComboStrategy: ctx.strategy, + }); + if (result.ok) { + ctx.log.info("COMBO", `Last-resort compat fallback succeeded via ${target.modelStr}`); + return result; + } + ctx.log.debug( + "COMBO", + `Last-resort compat fallback: ${target.modelStr} failed (${result.status}) — trying next` + ); + } + + return null; +} diff --git a/open-sse/services/combo/runtimeUnits.ts b/open-sse/services/combo/runtimeUnits.ts index 96951b81a2..d106ade336 100644 --- a/open-sse/services/combo/runtimeUnits.ts +++ b/open-sse/services/combo/runtimeUnits.ts @@ -2,7 +2,7 @@ import { errorResponse } from "../../utils/error.ts"; import { recordComboRequest } from "../comboMetrics.ts"; import { resolveDelayMs } from "./comboPredicates.ts"; -import { validateResponseQuality } from "./validateQuality.ts"; +import { validateResponseQuality, releaseQualityClone } from "./validateQuality.ts"; import type { ResponseValidationConfig } from "./responseValidation.ts"; import type { ComboCollectionLike, @@ -228,12 +228,19 @@ export async function executeRuntimeUnitCombo(args: { }); return { response, unit }; } + let unitClone: Response; + try { + unitClone = response.clone(); + } catch { + unitClone = response; + } const quality = await validateResponseQuality( - response, + unitClone, clientRequestedStream, args.log, args.config.responseValidation as ResponseValidationConfig | undefined ); + releaseQualityClone(unitClone, response, quality); if (quality.valid) { recordComboRequest(args.combo.name, unit.modelStr, { success: true, @@ -242,7 +249,7 @@ export async function executeRuntimeUnitCombo(args: { strategy: effectiveStrategy, target: { executionKey: unit.executionKey, stepId: unit.stepId, label: unit.label }, }); - return { response: quality.clonedResponse ?? response, unit }; + return { response, unit }; } } if (![408, 429, 500, 502, 503, 504].includes(response.status)) break; diff --git a/open-sse/services/combo/sessionStickiness.ts b/open-sse/services/combo/sessionStickiness.ts index 1b81548106..5bb8779a94 100644 --- a/open-sse/services/combo/sessionStickiness.ts +++ b/open-sse/services/combo/sessionStickiness.ts @@ -191,6 +191,26 @@ export function clearAllStickyBindings(): void { stickyMap.clear(); } +/** + * #6168: resolve the session-stickiness opt-out for a combo request. + * + * Precedence (mirrors the `stickyRoundRobinLimit` resolution in combo.ts): + * per-combo `config.disableSessionStickiness` (boolean) → + * global `settings.disableSessionStickiness` (boolean) → + * default `false`. + * + * Default `false` preserves the #3825 prompt-cache/504 fix — only an explicit + * `true` at either level disables stickiness. + */ +export function resolveDisableSessionStickiness( + config: Record | null | undefined, + settings: Record | null | undefined +): boolean { + const perCombo = config?.disableSessionStickiness; + if (typeof perCombo === "boolean") return perCombo; + return settings?.disableSessionStickiness === true; +} + // ─── Core: apply stickiness to an ordered target list ──────────────────────── export interface ApplyStickinessResult { diff --git a/open-sse/services/combo/types.ts b/open-sse/services/combo/types.ts index 766d6f71d9..290b0964d7 100644 --- a/open-sse/services/combo/types.ts +++ b/open-sse/services/combo/types.ts @@ -46,6 +46,8 @@ export type SingleModelTarget = allowRateLimitedConnection?: boolean; effectiveComboStrategy?: string | null; modelAbortSignal?: AbortSignal | null; + /** True when this target was selected via context-cache session pinning. */ + modelPinned?: boolean; }) | { modelAbortSignal: AbortSignal }; diff --git a/open-sse/services/combo/validateQuality.ts b/open-sse/services/combo/validateQuality.ts index fe7b00313e..2bab070a21 100644 --- a/open-sse/services/combo/validateQuality.ts +++ b/open-sse/services/combo/validateQuality.ts @@ -10,10 +10,7 @@ import { createSSEDataLineNormalizer, isKnownNonClaudeStreamPayload, } from "../../utils/streamHelpers.ts"; -import { - evaluateResponseValidation, - type ResponseValidationConfig, -} from "./responseValidation.ts"; +import { evaluateResponseValidation, type ResponseValidationConfig } from "./responseValidation.ts"; import { getReasoningTokens } from "../../../src/lib/usage/tokenAccounting.ts"; import type { ComboRetryAfter } from "./types.ts"; @@ -99,6 +96,8 @@ export async function validateResponseQuality( let hasMessageStart = false; let hasContentBlock = false; let hasLifecycleEnd = false; + let anyContentFound = false; + let sawAnyBytes = false; const sseLineNormalizer = createSSEDataLineNormalizer(); let pendingEventType = ""; @@ -236,6 +235,20 @@ export async function validateResponseQuality( return { valid: false, reason: "streaming empty content block" }; } + // Stream ended with a truly EMPTY body (e.g. Gemini returning HTTP + // 200 with zero bytes) — mark as invalid for combo failover so the + // sibling model gets tried. Streams that carried ANY SSE activity + // (an explicit `data: [DONE]`, ping/metadata events, an incomplete + // Claude lifecycle) keep the pass-through contract (#3399/#3685): + // those are handled by the stream-readiness timeout, not failover. + if (!anyContentFound && !hasContentBlock && !sawAnyBytes) { + log.warn?.( + "COMBO", + "Streaming response ended with no recognized content — marking as invalid for combo failover" + ); + return { valid: false, reason: "streaming no recognized content" }; + } + // Incomplete lifecycle or non-Claude stream — replay all buffered // bytes. The reader is exhausted so the forwarding reader will // immediately signal done. @@ -245,12 +258,14 @@ export async function validateResponseQuality( // Accumulate raw bytes for potential replay. bufferedChunks.push(value); + if (value && value.length > 0) sawAnyBytes = true; // Decode incrementally (stream:true keeps multi-byte char state). decodedSoFar += decoder.decode(value, { stream: true }); const foundContent = parseAccumulatedSse(); if (foundContent) { + anyContentFound = true; // A content_block_* event was found — stop peeking. Return a // clonedResponse that replays all buffered bytes (the current chunk // is already in bufferedChunks) and then forwards the remainder of @@ -259,9 +274,23 @@ export async function validateResponseQuality( return { valid: true, clonedResponse }; } } - } catch { - // If reading the stream fails, pass through — other mechanisms - // (stream readiness timeout) will catch truly broken streams. + } catch (streamErr) { + // If reading the stream fails due to a locked stream or pipe error, + // the content cannot be verified — mark as invalid for combo failover. + // A locked ReadableStream means the response body is already consumed + // or corrupted (e.g. "Invalid state: The ReadableStream is locked"). + // Broad match: Chrome/V8 throws "body used already", Firefox throws + // "ReadableStream is locked", etc. + const errMsg = streamErr instanceof Error ? streamErr.message : String(streamErr); + if ( + streamErr instanceof TypeError && + (errMsg.includes("locked") || + errMsg.includes("disturbed") || + errMsg.includes("used already")) + ) { + return { valid: false, reason: "stream locked or disturbed" }; + } + // Other read errors — pass through (stream readiness timeout will catch truly broken streams) return { valid: true }; } } @@ -308,7 +337,8 @@ export async function validateResponseQuality( const choices = json?.choices; if (json?.object === "response") { - if (!responsesApiOutputHasContent(json.output)) return { valid: false, reason: "empty_choices" }; + if (!responsesApiOutputHasContent(json.output)) + return { valid: false, reason: "empty_choices" }; const status = typeof json.status === "string" ? json.status : ""; if (status && !["completed", "done"].includes(status)) { return { valid: false, reason: "no_terminal" }; @@ -389,3 +419,27 @@ export async function validateResponseQuality( }), }; } + +/** + * Release the peek-and-abandon clone used by {@link validateResponseQuality}. + * + * The quality check clones the upstream response, reads the clone only until the + * first content block, then hands back a `clonedResponse` that callers on the + * streaming path DISCARD (they forward the original, untouched response). Because + * a `Response.clone()` tees the body, that abandoned branch would otherwise buffer + * the entire remaining body in memory until the original finishes streaming. + * + * Cancelling the abandoned branch releases that buffer. Per the ReadableStream tee + * contract, cancelling one branch does NOT cancel the shared source while the other + * branch (the original response being streamed to the client) is still active, so + * this is safe. No-op when the clone fell back to the original (clone unsupported) + * or when quality reading already exhausted the body (no `clonedResponse`). + */ +export function releaseQualityClone( + clone: Response, + original: Response, + quality: { clonedResponse?: Response } +): void { + if (clone === original) return; + void quality.clonedResponse?.body?.cancel().catch(() => {}); +} diff --git a/open-sse/services/pipeline.ts b/open-sse/services/pipeline.ts new file mode 100644 index 0000000000..3c1c86b06d --- /dev/null +++ b/open-sse/services/pipeline.ts @@ -0,0 +1,189 @@ +/** + * Pipeline combo strategy — sequential chain. + * + * A pipeline combo runs its targets IN ORDER: step N's output is fed into step + * N+1 as input, each step carries its own optional `prompt` (instruction), and + * only the FINAL step's response is returned to the client. This is the sequential + * counterpart to `fusion` (parallel fan-out + judge synthesis). + * + * ── Per-step config shape ───────────────────────────────────────────────────── + * The ordered step list IS `combo.models` — we reuse the existing target order + * rather than introducing a parallel `pipelineSteps` array that could drift out of + * sync with the models. Each step's optional instruction is read from a `prompt` + * field on the target object (`comboModelStepInputSchema.prompt`); a plain-string + * model entry is simply a step with no prompt. The field is optional and ignored by + * every other strategy, so this is fully backward-compatible. + * + * ── Prompt injection ────────────────────────────────────────────────────────── + * The engine passes prompts through the request's message array (OpenAI `messages`, + * Responses `input`, or Gemini `contents`). Each step's `prompt` is injected as a + * leading system instruction in whichever format the request uses: + * - step 1 keeps the client's original conversation and (if set) prepends its + * prompt as an extra system turn, so the first model sees the real user request; + * - steps 2..N are transforms — the conversation is replaced with the previous + * step's output as the user turn, plus this step's prompt as the system turn. + * + * Intermediate steps are forced non-streaming with tools stripped (we need the + * complete text to thread forward). The FINAL step keeps the client's original + * `stream` flag + tools, so streaming and downstream tool use still work. + * + * A step failure fails the whole pipeline EXPLICITLY (never silently swallowed): + * a non-OK intermediate response, an unparseable body, or an intermediate step that + * yields no text short-circuits with a sanitized error response. + */ +import { errorResponse } from "../utils/error.ts"; +import type { ComboLogger, HandleSingleModel } from "./combo/types.ts"; +// extractPanelText is a generic assistant-text extractor (OpenAI chat / Claude / +// Gemini / Responses) — reused here to read each step's output, not fusion-specific. +import { extractPanelText } from "./fusion.ts"; + +type Body = Record; + +export type PipelineStep = { model: string; prompt?: string | null }; + +/** + * Prepend a system instruction to the client's original conversation (format-aware), + * so step 1 sees the real user request plus its own step prompt. No-op when the + * step has no prompt. + */ +export function prependSystemInstruction(body: Body, prompt: string | null | undefined): Body { + const sys = typeof prompt === "string" && prompt.trim() ? prompt.trim() : null; + const next: Body = { ...body }; + if (!sys) return next; + if (Array.isArray(body.input)) { + next.input = [{ role: "system", content: sys }, ...(body.input as unknown[])]; + } else if (Array.isArray(body.contents)) { + // Gemini contents have no system role — a leading user turn is the closest analog. + next.contents = [{ role: "user", parts: [{ text: sys }] }, ...(body.contents as unknown[])]; + } else if (Array.isArray(body.messages)) { + next.messages = [{ role: "system", content: sys }, ...(body.messages as unknown[])]; + } else { + next.messages = [{ role: "system", content: sys }]; + } + return next; +} + +/** + * Replace the request's conversation with a fresh transform turn set (the previous + * step's output as the user turn + this step's prompt as the system turn), + * preserving whichever message-array shape the request format uses. Non-message + * fields are carried over; the caller overrides the model per step. + */ +export function buildTransformBody( + body: Body, + prompt: string | null | undefined, + input: string +): Body { + const next: Body = { ...body }; + const sys = typeof prompt === "string" && prompt.trim() ? prompt.trim() : null; + if (Array.isArray(body.input)) { + const turns: unknown[] = []; + if (sys) turns.push({ role: "system", content: sys }); + turns.push({ role: "user", content: input }); + next.input = turns; + delete next.messages; + delete next.contents; + } else if (Array.isArray(body.contents)) { + // Gemini contents have no system role — fold the instruction into the user turn. + const text = sys ? `${sys}\n\n${input}` : input; + next.contents = [{ role: "user", parts: [{ text }] }]; + delete next.messages; + delete next.input; + } else { + const turns: unknown[] = []; + if (sys) turns.push({ role: "system", content: sys }); + turns.push({ role: "user", content: input }); + next.messages = turns; + } + return next; +} + +/** Force non-streaming and strip tools so an intermediate step yields complete prose. */ +function stripStreaming(body: Body): Body { + const { tools: _tools, tool_choice: _tc, ...rest } = body; + void _tools; + void _tc; + return { ...rest, stream: false }; +} + +export type HandlePipelineChatOptions = { + body: Body; + steps: PipelineStep[]; + handleSingleModel: HandleSingleModel; + log: ComboLogger; + comboName?: string; +}; + +/** + * Handle a pipeline combo: run the steps in order, threading each step's output + * into the next step's input, and return only the final step's response. + */ +export async function handlePipelineChat({ + body, + steps, + handleSingleModel, + log, + comboName, +}: HandlePipelineChatOptions): Promise { + const chain = (Array.isArray(steps) ? steps : []).filter((s) => s && s.model); + if (chain.length === 0) { + return errorResponse(400, "Pipeline combo has no models"); + } + log.info( + "PIPELINE", + `Combo "${comboName ?? ""}" | steps=${chain.length} [${chain.map((s) => s.model).join(" -> ")}]` + ); + + // Single-step pipeline: nothing to chain — run it directly (streams to client). + if (chain.length === 1) { + return handleSingleModel(prependSystemInstruction(body, chain[0].prompt), chain[0].model); + } + + let prevOutput = ""; + for (let i = 0; i < chain.length; i++) { + const step = chain[i]; + const isFinal = i === chain.length - 1; + const isFirst = i === 0; + + let stepBody: Body = isFirst + ? prependSystemInstruction(body, step.prompt) + : buildTransformBody(body, step.prompt, prevOutput); + // Intermediate steps: complete prose only (no stream, no tools). The final step + // keeps the client's original stream flag + tools. + if (!isFinal) stepBody = stripStreaming(stepBody); + + const t0 = Date.now(); + const res = await handleSingleModel(stepBody, step.model); + + if (isFinal) { + log.info("PIPELINE", `Final step ${step.model} responded (${Date.now() - t0}ms)`); + return res; + } + + // An intermediate step must succeed with usable text — otherwise fail the whole + // pipeline (never silently swallow; the client gets a clear, sanitized error). + if (!res.ok) { + log.warn("PIPELINE", `Step ${i + 1} (${step.model}) failed`, { status: res.status }); + const status = res.status >= 400 && res.status <= 599 ? res.status : 502; + return errorResponse(status, `Pipeline step ${i + 1} (${step.model}) failed`); + } + try { + const json = await res.clone().json(); + prevOutput = extractPanelText(json); + } catch { + log.warn("PIPELINE", `Step ${i + 1} (${step.model}) returned an unparseable body`); + return errorResponse(502, `Pipeline step ${i + 1} (${step.model}) returned an unparseable body`); + } + if (!prevOutput.trim()) { + log.warn("PIPELINE", `Step ${i + 1} (${step.model}) returned empty output`); + return errorResponse(502, `Pipeline step ${i + 1} (${step.model}) returned empty output`); + } + log.info( + "PIPELINE", + `Step ${i + 1} ${step.model} ok (${prevOutput.length} chars, ${Date.now() - t0}ms)` + ); + } + + // Unreachable — the final step returns inside the loop. + return errorResponse(500, "Pipeline produced no final response"); +} diff --git a/open-sse/services/provider.ts b/open-sse/services/provider.ts index 9149c7e70b..9ba0a0acff 100644 --- a/open-sse/services/provider.ts +++ b/open-sse/services/provider.ts @@ -8,6 +8,7 @@ import { } from "./claudeCodeCompatible.ts"; import { getClaudeCodeCompatibleRequestDefaults } from "@/lib/providers/requestDefaults"; import { buildClineHeaders } from "@/shared/utils/clineAuth"; +import { usesCcWireImage } from "./ccWireImageBuiltins.ts"; const OPENAI_COMPATIBLE_PREFIX = "openai-compatible-"; const OPENAI_COMPATIBLE_DEFAULTS = { @@ -29,7 +30,12 @@ function isAnthropicCompatible(provider) { } export function isClaudeCodeCompatible(provider) { - return typeof provider === "string" && provider.startsWith(CLAUDE_CODE_COMPATIBLE_PREFIX); + return ( + (typeof provider === "string" && provider.startsWith(CLAUDE_CODE_COMPATIBLE_PREFIX)) || + // Built-in providers (e.g. agentrouter) that adopt the dynamic CC wire image + // while keeping their own registry baseUrl + auth (#6056). + usesCcWireImage(provider) + ); } export function getOpenAICompatibleType( @@ -256,6 +262,15 @@ export function buildProviderUrl( providerSpecificData?: Record | null; } = {} ) { + // Built-in CC-wire-image providers (e.g. agentrouter): keep the registry's + // OWN baseUrl (NOT the CC family's anthropic default) but adopt the CC chat + // path so the request still targets `?beta=true` (#6056). + if (usesCcWireImage(provider)) { + const entry = getRegistryEntry(provider); + const config = getProviderConfig(provider); + const baseUrl = options?.baseUrl || entry?.baseUrl || config.baseUrl; + return joinClaudeCodeCompatibleUrl(baseUrl, CLAUDE_CODE_COMPATIBLE_DEFAULT_CHAT_PATH); + } if (isOpenAICompatible(provider)) { const providerSpecificData = options?.providerSpecificData || null; const apiType = getOpenAICompatibleType(provider, providerSpecificData); @@ -318,12 +333,27 @@ export function buildProviderHeaders(provider, credentials, stream = true, body const ccRequestDefaults = getClaudeCodeCompatibleRequestDefaults( credentials?.providerSpecificData ); - return buildClaudeCodeCompatibleHeaders( + const ccHeaders = buildClaudeCodeCompatibleHeaders( token, stream, credentials?.providerSpecificData?.ccSessionId, { redactThinking: ccRequestDefaults.redactThinking === true } ); + // Built-in CC-wire-image providers (e.g. agentrouter): adopt the CC wire + // image headers but keep the registry's OWN auth scheme (e.g. x-api-key) + // instead of the CC family's Bearer auth (#6056). + if (usesCcWireImage(provider)) { + delete ccHeaders["Authorization"]; + const authHeader = entry?.authHeader || "bearer"; + if (authHeader === "x-api-key") { + if (token) ccHeaders["x-api-key"] = token; + } else if (authHeader === "key") { + if (token) ccHeaders["Authorization"] = `Key ${token}`; + } else { + ccHeaders["Authorization"] = `Bearer ${token}`; + } + } + return ccHeaders; } if (isAnthropicCompatible(provider)) { if (credentials.apiKey) { diff --git a/open-sse/services/qoderCli.ts b/open-sse/services/qoderCli.ts index 3aa110eee9..bccd50de6d 100644 --- a/open-sse/services/qoderCli.ts +++ b/open-sse/services/qoderCli.ts @@ -3,6 +3,9 @@ import crypto from "crypto"; import fs from "fs"; import os from "os"; import path from "path"; +import { getLookupEnv } from "@/shared/services/cliRuntime"; +import { buildQoderCliNotFoundHint, resolveQoderCliInvocation } from "./qoderCliResolve"; +export { getQoderCliCommand } from "./qoderCliResolve"; // #6263 public entry point const DEFAULT_TIMEOUT_MS = 45_000; const DEFAULT_MODELS_TIMEOUT_MS = 20_000; @@ -63,11 +66,6 @@ function getString(value: unknown): string { return typeof value === "string" ? value : ""; } -export function getQoderCliCommand(): string { - const explicit = String(process.env.CLI_QODER_BIN || "").trim(); - return explicit || "qodercli"; -} - export function getQoderCliWorkspace(): string { const explicit = String( process.env.QODER_CLI_WORKSPACE || process.env.OMNIROUTE_QODER_WORKSPACE || "" @@ -124,10 +122,13 @@ type SpawnQoderCliOptions = { * honors for headless PAT auth — and the prompt is piped through stdin so no * untrusted value is ever interpolated into a shell command (Hard Rule #13). */ -function spawnQoderCli(options: SpawnQoderCliOptions): Promise { - const command = String(options.command || "").trim() || getQoderCliCommand(); +async function spawnQoderCli(options: SpawnQoderCliOptions): Promise { const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS; - const env: NodeJS.ProcessEnv = { ...process.env }; + // #6263: resolve the real qodercli command (absolute .cmd/.exe on Windows) and + // whether it needs a shell, then spawn with the cliRuntime-enriched env (PATH + + // PATHEXT + APPDATA) so the npm `.cmd` wrapper under %APPDATA%\npm is found. + const { command, useShell } = await resolveQoderCliInvocation(options.command); + const env: NodeJS.ProcessEnv = { ...getLookupEnv() }; const token = String(options.token || "").trim(); if (token) env.QODER_PERSONAL_ACCESS_TOKEN = token; @@ -143,6 +144,7 @@ function spawnQoderCli(options: SpawnQoderCliOptions): Promise { + const explicit = String(explicitCommand || "").trim(); + const getStatus = deps.getStatus || getCliRuntimeStatus; + const shouldUseShell = deps.shouldUseShell || shouldUseShellForCommand; + // Only the default path is cached; an explicit per-call command or an injected + // resolver (tests) always resolves fresh and never touches the shared cache. + const cacheable = !explicit && !deps.getStatus && !deps.shouldUseShell; + const fallback = explicit || getQoderCliCommand(); + + if ( + cacheable && + qoderInvocationCache && + qoderInvocationCache.key === fallback && + qoderInvocationCache.expiresAt > Date.now() + ) { + return { command: qoderInvocationCache.command, useShell: qoderInvocationCache.useShell }; + } + + let command = fallback; + try { + const status = await getStatus("qoder"); + if (status && status.installed && status.commandPath) { + command = status.commandPath; + } + } catch { + /* fall back to the bare/explicit command — spawn will surface a real ENOENT */ + } + + const invocation: QoderCliInvocation = { command, useShell: shouldUseShell(command) }; + if (cacheable) { + qoderInvocationCache = { + ...invocation, + key: fallback, + expiresAt: Date.now() + QODER_RESOLVE_TTL_MS, + }; + } + return invocation; +} + +/** + * Build the operator-facing "qodercli not found" error, listing the paths the + * resolver searched plus the `CLI_QODER_BIN` override hint (#6263). + */ +export function buildQoderCliNotFoundHint(runError: string): string { + let searchedHint = ""; + try { + const candidates = getKnownToolPaths("qoder"); + if (candidates.length > 0) { + searchedHint = ` Searched: ${candidates.slice(0, 6).join(", ")}.`; + } + } catch { + /* best-effort — the path list is only advisory for the error message */ + } + return ( + `Qoder CLI (qodercli) was not found on the OmniRoute host (${runError}).` + + searchedHint + + " Install it from https://qoder.com, or set CLI_QODER_BIN to the absolute path " + + "of the qodercli binary (e.g. %APPDATA%\\npm\\qodercli.cmd on Windows). " + + "PAT auth is driven through the local qodercli binary." + ); +} diff --git a/open-sse/services/reasoningTokenBuffer.ts b/open-sse/services/reasoningTokenBuffer.ts index 65d021e7f5..bbb67da335 100644 --- a/open-sse/services/reasoningTokenBuffer.ts +++ b/open-sse/services/reasoningTokenBuffer.ts @@ -1,5 +1,14 @@ import { getResolvedModelCapabilities } from "../../src/lib/modelCapabilities.ts"; +/** + * Below this caller-supplied `max_tokens`, the request is treated as a probe + * (e.g. Claude Code's `/model` capability check sends `max_tokens: 1`) rather + * than a genuine reasoning budget, so no headroom is added. Keeping it a named + * constant makes the threshold easy to tune. See issue #6274 (probe inflated to + * 1001 upstream) vs. issue #3587 (headroom for real reasoning budgets). + */ +export const REASONING_BUFFER_MIN_TRIGGER = 256; + export function toPositiveInteger(value: unknown): number | null { const numericValue = typeof value === "number" @@ -30,6 +39,10 @@ export function resolveReasoningBufferedMaxTokens( if (current > maxOutputTokens) return maxOutputTokens; if (current === maxOutputTokens) return current; + // Issue #6274: a tiny explicit budget is a capability probe, not a reasoning + // request. Respect it verbatim instead of inflating (e.g. 1 -> 1001). + if (current < REASONING_BUFFER_MIN_TRIGGER) return current; + const buffered = Math.max(current + 1000, Math.ceil(current * 1.5)); if (buffered > maxOutputTokens) return current; diff --git a/open-sse/services/toolSchemaSanitizer.ts b/open-sse/services/toolSchemaSanitizer.ts index 76a25ef28f..60853e213c 100644 --- a/open-sse/services/toolSchemaSanitizer.ts +++ b/open-sse/services/toolSchemaSanitizer.ts @@ -110,8 +110,31 @@ function sanitizeSchema(value: unknown, depth = 0): Record { return result; } +/** + * OpenAI's Responses API strict validator requires the ROOT parameters schema to + * declare `type: "object"` explicitly. Clients like the Codex app emit + * `type: null` (rejected upstream as: schema must be a JSON Schema of + * 'type: "object"', got 'type: null' — issue #6359). sanitizeSchema drops the + * null, so at the root we re-add the mandatory "object". Combinator roots + * (anyOf/oneOf/allOf) are left alone — injecting a sibling `type` would change + * their meaning — and explicit root types are preserved as-is. + */ +function ensureRootObjectType(schema: Record): void { + if (hasOwn(schema, "type")) return; + if (hasOwn(schema, "anyOf") || hasOwn(schema, "oneOf") || hasOwn(schema, "allOf")) return; + schema.type = "object"; + if (!isPlainObject(schema.properties)) { + schema.properties = {}; + if (!hasOwn(schema, "additionalProperties")) schema.additionalProperties = true; + } +} + function normalizeParameters(parameters: unknown): unknown { - if (isPlainObject(parameters)) return sanitizeSchema(parameters); + if (isPlainObject(parameters)) { + const sanitized = sanitizeSchema(parameters); + ensureRootObjectType(sanitized); + return sanitized; + } if (parameters === null || parameters === undefined) { return { type: "object", properties: {}, additionalProperties: true }; } diff --git a/open-sse/translator/paramSupport.ts b/open-sse/translator/paramSupport.ts index a0a3a70bab..30a32e2b5c 100644 --- a/open-sse/translator/paramSupport.ts +++ b/open-sse/translator/paramSupport.ts @@ -29,9 +29,12 @@ const STRIP_RULES: StripRule[] = [ /claude/i.test(m) && !/claude.*(opus|sonnet).*4\.6/i.test(m), drop: ["thinking", "reasoning_effort"], }, - // NVIDIA NIM z-ai/glm-5.2: OpenAI-compatible wrapper rejects the `reasoning` - // body field → HTTP 400 "Unsupported parameter(s): `reasoning`". #6102 drop pattern. - { provider: "nvidia", match: /z-ai\/glm-5\.2\b/i, drop: ["reasoning"] }, + // NVIDIA NIM z-ai/glm-5.2: OpenAI-compatible wrapper rejects BOTH the `reasoning` + // body field (#6102) and the Claude-style `thinking` field. A Claude-format + // client (e.g. Claude Code) routed here leaves a `thinking:{type:"adaptive"}` + // that the wrapper 400s on — same class already handled for minimax-m2.7 below. + // 9router#2023. + { provider: "nvidia", match: /z-ai\/glm-5\.2\b/i, drop: ["reasoning", "thinking"] }, // NVIDIA NIM minimaxai/minimax-m2.7: NVIDIA's OpenAI-compatible wrapper // (format:"openai") does not accept the Claude-style `thinking` body field // and returns 400 "Unsupported parameter(s): thinking". Upstream #2268. diff --git a/open-sse/translator/request/gemini-to-openai.ts b/open-sse/translator/request/gemini-to-openai.ts index 4e968e2671..f073eaf288 100644 --- a/open-sse/translator/request/gemini-to-openai.ts +++ b/open-sse/translator/request/gemini-to-openai.ts @@ -46,7 +46,7 @@ export function geminiToOpenAIRequest(model, body, stream) { // Convert contents to messages if (body.contents && Array.isArray(body.contents)) { - for (const content of body.contents) { + for (const content of splitCoLocatedFunctionResponses(body.contents)) { const converted = convertGeminiContent(content); if (converted) { result.messages.push(converted); @@ -77,6 +77,38 @@ export function geminiToOpenAIRequest(model, body, stream) { } // Convert Gemini content to OpenAI message +// convertGeminiContent() early-returns the tool message on the first +// `functionResponse` part in a content, dropping any co-located parts +// (another functionCall, or trailing text). Gemini clients can send those +// co-located. Pre-split each such content into: one single-part content per +// functionResponse (each early-returns cleanly as a tool message, emitted +// first to keep tool-result-before-next-turn ordering) plus one content for +// the remaining non-functionResponse parts. +function splitCoLocatedFunctionResponses(contents) { + const out = []; + for (const content of contents) { + if (!content || !Array.isArray(content.parts)) { + out.push(content); + continue; + } + const hasFunctionResponse = content.parts.some((p) => p && p.functionResponse); + if (!hasFunctionResponse) { + out.push(content); + continue; + } + for (const part of content.parts) { + if (part && part.functionResponse) { + out.push({ ...content, parts: [part] }); + } + } + const nonFRParts = content.parts.filter((p) => !(p && p.functionResponse)); + if (nonFRParts.length > 0) { + out.push({ ...content, parts: nonFRParts }); + } + } + return out; +} + function convertGeminiContent(content) { const role = content.role === "user" ? "user" : "assistant"; diff --git a/open-sse/translator/response/openai-to-gemini-sse.ts b/open-sse/translator/response/openai-to-gemini-sse.ts index 430f385a05..6613d061c9 100644 --- a/open-sse/translator/response/openai-to-gemini-sse.ts +++ b/open-sse/translator/response/openai-to-gemini-sse.ts @@ -37,10 +37,18 @@ export const OPENAI_TO_GEMINI_FINISH_REASON: Record = { content_filter: "SAFETY", }; +interface OpenAIToolCallDelta { + index?: number; + id?: string; + type?: string; + function?: { name?: string; arguments?: string }; +} + interface OpenAIChoiceDelta { content?: string | null; reasoning_content?: string | null; role?: string; + tool_calls?: OpenAIToolCallDelta[]; } interface OpenAIChoice { @@ -63,9 +71,31 @@ interface OpenAIStreamChunk { model?: string; } +interface GeminiFunctionCall { + name: string; + args: Record; +} + +interface GeminiFunctionResponse { + name: string; + response: Record; +} + interface GeminiPart { - text: string; + text?: string; thought?: boolean; + functionCall?: GeminiFunctionCall; + functionResponse?: GeminiFunctionResponse; +} + +/** + * Per-stream mutable accumulator for OpenAI streamed tool calls. OpenAI emits + * a tool call's `arguments` as partial JSON fragments across several delta + * chunks, keyed by `index`; we buffer them here and flush a complete + * `functionCall` part once `finish_reason` arrives. + */ +export interface GeminiToolCallState { + toolCallAccum?: Record; } interface GeminiCandidate { @@ -97,7 +127,8 @@ interface GeminiStreamChunk { */ export function openAIChunkToGeminiChunk( parsed: OpenAIStreamChunk, - fallbackModel: string + fallbackModel: string, + state?: GeminiToolCallState ): GeminiStreamChunk | null { const choice = parsed.choices?.[0]; if (!choice) return null; @@ -112,6 +143,35 @@ export function openAIChunkToGeminiChunk( parts.push({ text: String(delta.content) }); } + // Accumulate streamed tool-call fragments (OpenAI streams partial JSON + // `arguments` across chunks, keyed by index). Requires a caller-supplied + // per-stream `state`; nothing is emitted until finish_reason arrives. + if (state && Array.isArray(delta.tool_calls)) { + const accum = (state.toolCallAccum ??= {}); + for (const tc of delta.tool_calls) { + const idx = tc.index ?? 0; + const entry = (accum[idx] ??= { id: "", name: "", arguments: "" }); + if (tc.id) entry.id = tc.id; + if (tc.function?.name) entry.name += tc.function.name; + if (tc.function?.arguments) entry.arguments += tc.function.arguments; + } + } + + // On finish, flush accumulated tool calls as complete functionCall parts. + if (choice.finish_reason && state?.toolCallAccum) { + for (const key of Object.keys(state.toolCallAccum)) { + const entry = state.toolCallAccum[Number(key)]; + if (!entry.name) continue; + let args: Record = {}; + try { + args = JSON.parse(entry.arguments || "{}") as Record; + } catch { + args = {}; + } + parts.push({ functionCall: { name: entry.name, args } }); + } + } + // Skip pure role-only deltas with no content and no finish signal. if (parts.length === 0 && !choice.finish_reason) return null; @@ -168,6 +228,9 @@ export function transformOpenAISSEToGeminiSSE(upstreamResponse: Response, model: // chunk so we never JSON.parse a half-event. let buffer = ""; + // Per-stream tool-call accumulator (shared across transform + flush). + const toolCallState: GeminiToolCallState = {}; + const transform = new TransformStream({ transform(chunk, controller) { buffer += decoder.decode(chunk, { stream: true }); @@ -192,7 +255,7 @@ export function transformOpenAISSEToGeminiSSE(upstreamResponse: Response, model: continue; } - const geminiChunk = openAIChunkToGeminiChunk(parsed, model); + const geminiChunk = openAIChunkToGeminiChunk(parsed, model, toolCallState); if (!geminiChunk) continue; controller.enqueue(encoder.encode("data: " + JSON.stringify(geminiChunk) + "\r\n\r\n")); @@ -212,7 +275,7 @@ export function transformOpenAISSEToGeminiSSE(upstreamResponse: Response, model: } catch { return; } - const geminiChunk = openAIChunkToGeminiChunk(parsed, model); + const geminiChunk = openAIChunkToGeminiChunk(parsed, model, toolCallState); if (!geminiChunk) return; controller.enqueue(encoder.encode("data: " + JSON.stringify(geminiChunk) + "\r\n\r\n")); }, @@ -228,10 +291,17 @@ export function transformOpenAISSEToGeminiSSE(upstreamResponse: Response, model: }); } +interface OpenAIToolCall { + id?: string; + type?: string; + function?: { name?: string; arguments?: string }; +} + interface OpenAIMessage { content?: string | null; reasoning_content?: string | null; role?: string; + tool_calls?: OpenAIToolCall[]; } interface OpenAINonStreamChoice { @@ -311,7 +381,22 @@ export async function convertOpenAIResponseToGemini( if (message.reasoning_content) { parts.push({ text: String(message.reasoning_content), thought: true }); } - parts.push({ text: String(message.content ?? "") }); + // Text content — only emit a text part when there is actual text, so a + // pure tool-call response isn't padded with an empty-string part. + const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : []; + if (message.content || toolCalls.length === 0) { + parts.push({ text: String(message.content ?? "") }); + } + // Tool calls → functionCall parts (arguments JSON string → object). + for (const tc of toolCalls) { + let args: Record = {}; + try { + args = JSON.parse(tc.function?.arguments || "{}") as Record; + } catch { + args = {}; + } + parts.push({ functionCall: { name: tc.function?.name || "", args } }); + } const finishReason = OPENAI_TO_GEMINI_FINISH_REASON[finish_reason ?? "stop"] ?? "STOP"; diff --git a/open-sse/utils/earlyStreamKeepalive.ts b/open-sse/utils/earlyStreamKeepalive.ts index db2b98f194..c70180f927 100644 --- a/open-sse/utils/earlyStreamKeepalive.ts +++ b/open-sse/utils/earlyStreamKeepalive.ts @@ -54,6 +54,8 @@ export type EarlyStreamKeepaliveOptions = { * for their stream watchdog and only a real `event: ping` keeps them from aborting. */ keepaliveFrame?: Uint8Array; + /** Extra headers to include in the keepalive response (e.g. X-Correlation-Id). */ + extraHeaders?: Record; }; type SettledHandler = { ok: true; response: Response } | { ok: false; error: unknown }; @@ -66,6 +68,7 @@ export async function withEarlyStreamKeepalive( const intervalMs = Math.max(250, options.intervalMs ?? 2_500); const signal = options.signal ?? null; const keepaliveFrame = options.keepaliveFrame ?? KEEPALIVE_FRAME; + const extraHeaders = options.extraHeaders ?? {}; // Settle into a tagged result so neither race branch leaves an unhandled // rejection when the threshold timer wins. @@ -154,10 +157,25 @@ export async function withEarlyStreamKeepalive( if (response.body && isSse) { // Real SSE stream — forward it verbatim. upstreamReader = response.body.getReader(); - while (true) { - const { done, value } = await upstreamReader.read(); - if (done) break; - if (value) controller.enqueue(value); + let bytesForwarded = 0; + try { + while (true) { + const { done, value } = await upstreamReader.read(); + if (done) break; + if (value) { + controller.enqueue(value); + bytesForwarded += value.byteLength; + } + } + } catch (readErr) { + // Upstream stream failed mid-flight. Only emit an error frame if + // NO content was forwarded yet — otherwise the client already + // received partial content and a late error frame would corrupt + // the SSE stream. Silently close instead; the client will see + // the stream end naturally. + if (bytesForwarded === 0) { + controller.enqueue(ERROR_FRAME); + } } } else { // Non-SSE response (e.g. a JSON error) reached us after we already @@ -204,6 +222,7 @@ export async function withEarlyStreamKeepalive( "Content-Type": "text/event-stream; charset=utf-8", "Cache-Control": "no-cache, no-transform", Connection: "keep-alive", + ...extraHeaders, }, }); } diff --git a/open-sse/utils/finishReason.ts b/open-sse/utils/finishReason.ts index 79221ad07f..5d8ab33667 100644 --- a/open-sse/utils/finishReason.ts +++ b/open-sse/utils/finishReason.ts @@ -13,6 +13,7 @@ const SAFETY_FINISH_REASONS = new Set([ "prohibited_content", "content_filtered", "policy_violation", + "malformed_response", ]); export function normalizeOpenAICompatibleFinishReason(value: unknown): unknown { diff --git a/open-sse/utils/opencodeHeaders.ts b/open-sse/utils/opencodeHeaders.ts index 17746522a4..4e1221877c 100644 --- a/open-sse/utils/opencodeHeaders.ts +++ b/open-sse/utils/opencodeHeaders.ts @@ -32,11 +32,19 @@ function findHeader(headers: Record, name: string): string | und * @param options.synthesizeRequestId - When true (OpencodeExecutor only), maps * x-session-affinity / x-session-id to x-opencode-session when the latter is * missing, and synthesizes a UUID for x-opencode-request if also missing. + * @param options.cliDefaults - When provided (OpencodeExecutor only), synthesize + * the OpenCode CLI identity headers that Cloudflare requires on VPS egress + * (User-Agent, x-opencode-client, x-opencode-project) plus fresh request/session + * UUIDs, but ONLY for keys the client did not already supply. Client values always + * win; these defaults only fill gaps. (#5997) */ export function forwardOpencodeClientHeaders( headers: Record, clientHeaders: Record, - options?: { synthesizeRequestId?: boolean } + options?: { + synthesizeRequestId?: boolean; + cliDefaults?: { userAgent: string; client: string; project: string }; + } ): void { // 1. Forward User-Agent const clientUA = clientHeaders["User-Agent"] || clientHeaders["user-agent"]; @@ -64,4 +72,27 @@ export function forwardOpencodeClientHeaders( } } } + + // 4. OpencodeExecutor-only: synthesize the OpenCode CLI identity Cloudflare expects + // on VPS egress, for any key the client did not supply (#5997). + if (options?.cliDefaults) { + applyCliDefaults(headers, options.cliDefaults); + } +} + +/** + * Fill the OpenCode CLI identity headers Cloudflare requires on VPS egress, but only for + * keys the client did not already supply (client values always win). (#5997) + */ +function applyCliDefaults( + headers: Record, + cliDefaults: { userAgent: string; client: string; project: string } +): void { + if (!headers["User-Agent"] && !headers["user-agent"]) { + setUserAgentHeader(headers, cliDefaults.userAgent); + } + headers["x-opencode-client"] ||= cliDefaults.client; + headers["x-opencode-project"] ||= cliDefaults.project; + headers["x-opencode-request"] ||= randomUUID(); + headers["x-opencode-session"] ||= randomUUID(); } diff --git a/open-sse/utils/proxyDispatcher.ts b/open-sse/utils/proxyDispatcher.ts index 718319b8e1..2d2e84bf02 100644 --- a/open-sse/utils/proxyDispatcher.ts +++ b/open-sse/utils/proxyDispatcher.ts @@ -59,6 +59,21 @@ function getDispatcherOptions() { // keepAliveTimeout UP to undici's default keepAliveMaxTimeout (600 s), // completely overriding the configured 1 s and restoring zombie-socket risk. keepAliveMaxTimeout: timeouts.fetchKeepAliveTimeoutMs, + // 9router#1237: RFC 8305 Happy Eyeballs. undici does not + // enable it by default, so when DNS returns both AAAA (IPv6) and A (IPv4) + // and the IPv6 route is broken (e.g. NAT64 `64:ff9b::` without routing), + // the direct egress connect hangs until ETIMEDOUT — even though `curl` + // (which has Happy Eyeballs) reaches the same host. Race both families and + // use whichever connects first. The proxy path pins family via `proxyTls` + // and ProxyAgent ignores `connect`, so this only affects direct egress. + // undici types `connect` as a union whose TcpNetConnectOpts member nominally + // requires `port`; at runtime undici merges these into net.connect (the origin + // already carries host:port), so the partial pin is valid — cast to suppress + // the spurious missing-`port` error, mirroring the `proxyTls` cast below. + connect: { + autoSelectFamily: true, + autoSelectFamilyAttemptTimeout: 1000, + } as ProxyAgent.Options["proxyTls"], }; } diff --git a/open-sse/utils/stream.ts b/open-sse/utils/stream.ts index 8357ce2506..120644ac9d 100644 --- a/open-sse/utils/stream.ts +++ b/open-sse/utils/stream.ts @@ -30,7 +30,9 @@ import { STREAM_IDLE_TIMEOUT_MS, FETCH_BODY_TIMEOUT_MS, HTTP_STATUS } from "../c import { OMIT_STREAMING_CHUNK_MARKER, sanitizeStreamingChunk, + isResponsesCommentaryMessageItem, } from "../handlers/responseSanitizer.ts"; +import { isFeatureFlagEnabled } from "@/shared/utils/featureFlags"; import { buildErrorBody } from "./error.ts"; import { parseTextualToolCallCandidate, isValidToolCallHeaderPrefix } from "./textualToolCall.ts"; import { recordToolLatency } from "../services/toolLatencyTracker.ts"; @@ -119,6 +121,12 @@ type StreamOptions = { copilotCompatibleReasoning?: boolean; /** Suppress the `` close marker for clients that render it verbatim (#5245). */ suppressThinkClose?: boolean; + /** + * Drop internal commentary-phase output items from Responses API passthrough + * streams before forwarding (#6199). When omitted, falls back to the + * `RESPONSES_PASSTHROUGH_DROP_COMMENTARY` feature flag (default on). + */ + dropResponsesCommentary?: boolean; provider?: string | null; reqLogger?: StreamLogger | null; toolNameMap?: unknown; @@ -621,9 +629,16 @@ export function createSSEStream(options: StreamOptions = {}) { body = null, onComplete = null, onFailure = null, + dropResponsesCommentary, } = options; const signatureNamespace = connectionId; + // Drop internal commentary-phase Responses output before forwarding (#6199). + // Explicit option wins; otherwise read the feature flag (default on). Resolved + // once per stream — never on the hot per-chunk path. + const shouldDropResponsesCommentary = + dropResponsesCommentary ?? isFeatureFlagEnabled("RESPONSES_PASSTHROUGH_DROP_COMMENTARY"); + const clientExpectsResponsesStream = (mode === STREAM_MODE.PASSTHROUGH ? clientResponseFormat === FORMATS.OPENAI_RESPONSES @@ -684,6 +699,12 @@ export function createSSEStream(options: StreamOptions = {}) { let passthroughResponsesId: string | null = null; let passthroughResponsesCurrentFunctionCallKey: string | null = null; const passthroughResponsesReasoningSummarySeen = new Set(); + // #6199 — commentary-phase items announced via `response.output_item.added` are + // internal. Their `response.output_text.delta`/`response.output_text.done`/ + // `response.output_item.done` events do not carry the `phase`, so we remember the + // item id + output_index here and drop every matching follow-up event. + const passthroughResponsesCommentaryItemIds = new Set(); + const passthroughResponsesCommentaryIndexes = new Set(); // #5786 — highest Responses-API `sequence_number` already forwarded on this stream. // The Responses API guarantees a strictly increasing sequence_number, so any event at // or below this watermark is an upstream reconnect/retry replay and must be dropped — @@ -1287,6 +1308,50 @@ export function createSSEStream(options: StreamOptions = {}) { parsed.type === "error"); if (isResponsesSSE) { + // #6199 — statefully drop internal commentary-phase output. The + // `response.output_item.added` announces the phase; the follow-up + // delta/done events only carry `item_id`/`output_index`, so we key + // off those. Happy-path (non-commentary) events are untouched. + if (shouldDropResponsesCommentary) { + const responsesEventType = parsed.type as string; + const eventOutputIndex = + typeof parsed.output_index === "number" ? parsed.output_index : null; + const eventItem = + parsed.item && typeof parsed.item === "object" && !Array.isArray(parsed.item) + ? (parsed.item as JsonRecord) + : null; + const eventItemId = + typeof parsed.item_id === "string" + ? parsed.item_id + : eventItem && typeof eventItem.id === "string" + ? eventItem.id + : null; + + if ( + responsesEventType === "response.output_item.added" && + isResponsesCommentaryMessageItem(parsed.item) + ) { + if (eventItemId) passthroughResponsesCommentaryItemIds.add(eventItemId); + if (eventOutputIndex !== null) + passthroughResponsesCommentaryIndexes.add(eventOutputIndex); + continue; + } + + const belongsToCommentary = + (eventItemId !== null && + passthroughResponsesCommentaryItemIds.has(eventItemId)) || + (eventOutputIndex !== null && + passthroughResponsesCommentaryIndexes.has(eventOutputIndex)); + if (belongsToCommentary) { + if (responsesEventType === "response.output_item.done") { + if (eventItemId) passthroughResponsesCommentaryItemIds.delete(eventItemId); + if (eventOutputIndex !== null) + passthroughResponsesCommentaryIndexes.delete(eventOutputIndex); + } + continue; + } + } + const responsesIdsNormalized = normalizeResponsesSseIds(parsed as JsonRecord); const parsedResponse = parsed.response && diff --git a/open-sse/utils/thinkCloseMarker.ts b/open-sse/utils/thinkCloseMarker.ts index bea9a75c49..44ac02c4e2 100644 --- a/open-sse/utils/thinkCloseMarker.ts +++ b/open-sse/utils/thinkCloseMarker.ts @@ -28,7 +28,11 @@ 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"]; +// - `opencode` (#5245): renders the marker as literal text. +// - `antigravity` (#1061): the Antigravity IDE client (UA +// `vscode/ (Antigravity/)`) renders a bare `` as the sole +// visible content on thinking-only turns, which trips its loop-detection. +const SUPPRESS_THINK_CLOSE_UA_MARKERS = ["opencode", "antigravity"]; /** * Whether the streamed `` close marker should be suppressed for the diff --git a/package-lock.json b/package-lock.json index a9c4af7c2d..be0d7d80f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "omniroute", - "version": "3.8.45", + "version": "3.8.46", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "omniroute", - "version": "3.8.45", + "version": "3.8.46", "hasInstallScript": true, "license": "MIT", "workspaces": [ @@ -103,7 +103,7 @@ "@testing-library/react": "^16.3.2", "@types/better-sqlite3": "^7.6.13", "@types/bun": "latest", - "@types/node": "^26.0.0", + "@types/node": "^26.1.0", "@types/react": "^19.2.15", "@types/react-dom": "^19.2.3", "@types/safe-regex": "^1.1.6", @@ -9500,9 +9500,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.0.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.1.tgz", - "integrity": "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==", + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.0.tgz", + "integrity": "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==", "devOptional": true, "license": "MIT", "dependencies": { @@ -28673,7 +28673,7 @@ }, "open-sse": { "name": "@omniroute/open-sse", - "version": "3.8.45", + "version": "3.8.46", "dependencies": { "@toon-format/toon": "^2.3.0", "safe-regex": "^2.1.1" diff --git a/package.json b/package.json index 4b5e40b5f3..8f62f43fa3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "omniroute", - "version": "3.8.45", + "version": "3.8.46", "description": "Unified AI router with 160+ providers, RTK+Caveman compression, auto fallback, MCP/A2A, desktop, PWA, and OpenAI-compatible APIs.", "type": "module", "bin": { @@ -97,13 +97,13 @@ "electron:build:linux": "npm run build && cd electron && npm run build:linux", "electron:smoke:packaged": "node scripts/dev/smoke-electron-packaged.mjs", "test": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-concurrency=20 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\" \"tests/unit/**/*.test.mjs\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-concurrency=20 \"tests/unit/dashboard/**/*.test.ts\"", - "test:unit": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=20 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\" \"tests/unit/**/*.test.mjs\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=20 \"tests/unit/dashboard/**/*.test.ts\"", - "test:unit:ci": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\" \"tests/unit/**/*.test.mjs\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 \"tests/unit/dashboard/**/*.test.ts\"", - "test:unit:ci:shard": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=4096 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=$TEST_SHARD tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\" \"tests/unit/**/*.test.mjs\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=4096 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=$TEST_SHARD \"tests/unit/dashboard/**/*.test.ts\"", - "test:unit:fast": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-isolation=none tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\" \"tests/unit/**/*.test.mjs\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-isolation=none \"tests/unit/dashboard/**/*.test.ts\"", + "test:unit": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=20 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\" \"tests/unit/**/*.test.mjs\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=20 \"tests/unit/dashboard/**/*.test.ts\" && npm run test:unit:serial", + "test:unit:ci": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\" \"tests/unit/**/*.test.mjs\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 \"tests/unit/dashboard/**/*.test.ts\" && npm run test:unit:serial", + "test:unit:ci:shard": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=4096 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=$TEST_SHARD tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\" \"tests/unit/**/*.test.mjs\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=4096 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=$TEST_SHARD \"tests/unit/dashboard/**/*.test.ts\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=4096 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=1 --test-shard=$TEST_SHARD \"tests/unit/serial/**/*.test.ts\"", + "test:unit:fast": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-isolation=none tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\" \"tests/unit/**/*.test.mjs\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-isolation=none \"tests/unit/dashboard/**/*.test.ts\" && npm run test:unit:serial", "test:unit:shard": "concurrently --kill-others-on-fail -n s1,s2 \"npm:test:unit:shard:1\" \"npm:test:unit:shard:2\"", - "test:unit:shard:1": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=10 --test-shard=1/2 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\" \"tests/unit/**/*.test.mjs\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=10 --test-shard=1/2 \"tests/unit/dashboard/**/*.test.ts\"", - "test:unit:shard:2": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=10 --test-shard=2/2 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\" \"tests/unit/**/*.test.mjs\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=10 --test-shard=2/2 \"tests/unit/dashboard/**/*.test.ts\"", + "test:unit:shard:1": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=10 --test-shard=1/2 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\" \"tests/unit/**/*.test.mjs\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=10 --test-shard=1/2 \"tests/unit/dashboard/**/*.test.ts\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=1 --test-shard=1/2 \"tests/unit/serial/**/*.test.ts\"", + "test:unit:shard:2": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=10 --test-shard=2/2 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\" \"tests/unit/**/*.test.mjs\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=10 --test-shard=2/2 \"tests/unit/dashboard/**/*.test.ts\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=1 --test-shard=2/2 \"tests/unit/serial/**/*.test.ts\"", "test:plan3": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test tests/unit/plan3-p0.test.ts", "test:fixes": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test tests/unit/fixes-p1.test.ts", "test:security": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test tests/unit/security-fase01.test.ts", @@ -143,6 +143,8 @@ "check:tracked-artifacts": "node scripts/check/check-tracked-artifacts.mjs", "check:test-masking": "node scripts/check/check-test-masking.mjs", "check:test-runner-api": "node scripts/check/check-test-runner-api.mjs", + "check:changelog-integrity": "node scripts/check/check-changelog-integrity.mjs", + "check:agent-skills-sync": "node --import tsx/esm scripts/skills/generate-agent-skills.mjs", "check:build-scope": "node scripts/check/check-build-scope.mjs", "check:error-helper": "node scripts/check/check-error-helper.mjs", "check:migration-numbering": "node scripts/check/check-migration-numbering.mjs", @@ -210,7 +212,8 @@ "build:cli-api": "node --import tsx/esm scripts/cli/generate-api-commands.mjs", "release:contributors": "node scripts/release/gen-contributors.mjs", "release:uncovered": "node scripts/release/list-uncovered-commits.mjs", - "test:coverage:runner": "node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=8 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\" \"tests/unit/**/*.test.mjs\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true NODE_OPTIONS=--max-old-space-size=8192 c8 --merge-async --output-dir coverage --exclude=tests/** --exclude=**/*.test.* --reporter=text-summary --reporter=html --reporter=json-summary --reporter=lcov --check-coverage --statements 60 --lines 60 --functions 60 --branches 60 node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=8 \"tests/unit/dashboard/**/*.test.ts\"" + "test:coverage:runner": "node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=8 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\" \"tests/unit/**/*.test.mjs\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true NODE_OPTIONS=--max-old-space-size=8192 c8 --merge-async --output-dir coverage --exclude=tests/** --exclude=**/*.test.* --reporter=text-summary --reporter=html --reporter=json-summary --reporter=lcov --check-coverage --statements 60 --lines 60 --functions 60 --branches 60 node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=8 \"tests/unit/dashboard/**/*.test.ts\" && npm run test:unit:serial", + "test:unit:serial": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=4096 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=1 \"tests/unit/serial/**/*.test.ts\"" }, "dependencies": { "@aws-sdk/client-bedrock-runtime": "^3.1073.0", @@ -309,7 +312,7 @@ "@testing-library/react": "^16.3.2", "@types/better-sqlite3": "^7.6.13", "@types/bun": "latest", - "@types/node": "^26.0.0", + "@types/node": "^26.1.0", "@types/react": "^19.2.15", "@types/react-dom": "^19.2.3", "@types/safe-regex": "^1.1.6", diff --git a/scripts/build/build-next-isolated.mjs b/scripts/build/build-next-isolated.mjs index 28681bb299..09dc26bb89 100644 --- a/scripts/build/build-next-isolated.mjs +++ b/scripts/build/build-next-isolated.mjs @@ -108,7 +108,12 @@ function runNextBuild() { } export function resolveNextBuildBundlerFlag(baseEnv = process.env) { - return baseEnv.OMNIROUTE_USE_TURBOPACK === "1" ? "--turbopack" : "--webpack"; + // Turbopack is the default production bundler (Next 16 stable). Benchmarked on + // this codebase: 2-3x faster than the single-threaded webpack pass (17min -> 9min + // on a 32-core box; ~20min -> 7min on ubuntu-latest), artifact validated + // end-to-end (standalone smoke + e2e/package/electron CI jobs). Webpack stays as + // the explicit escape hatch (=0) for bundler-compat regressions. + return baseEnv.OMNIROUTE_USE_TURBOPACK === "0" ? "--webpack" : "--turbopack"; } export function resolveNextBuildEnv(baseEnv = process.env) { diff --git a/scripts/build/mitm-stub-flag.mjs b/scripts/build/mitm-stub-flag.mjs new file mode 100644 index 0000000000..606dbcede4 --- /dev/null +++ b/scripts/build/mitm-stub-flag.mjs @@ -0,0 +1,23 @@ +/** + * Decide whether the Turbopack build should alias @/mitm/manager to the + * feature-degraded stub (src/mitm/manager.stub.ts). + * + * History (#6344): the alias used to be UNCONDITIONAL in next.config.mjs + * because Docker images were the only Turbopack consumers (webpack was the + * production default and never aliased the manager). When v3.8.45 flipped the + * production bundler default to Turbopack, the stub silently shipped to every + * npm / Electron / VPS artifact — Agent Bridge start then threw + * "MITM manager stub reached at runtime" for all non-Docker users. + * + * The stub is only correct where the runtime genuinely cannot run the MITM + * stack (containers without host access — #3390 graceful degradation), so it + * is now opt-in via OMNIROUTE_MITM_STUB=1, set by the Dockerfile. + */ +export function shouldStubMitmManager(env = process.env) { + return env.OMNIROUTE_MITM_STUB === "1"; +} + +/** Turbopack resolveAlias fragment for @/mitm/manager, derived from the env. */ +export function mitmManagerAliasFor(env = process.env) { + return shouldStubMitmManager(env) ? { "@/mitm/manager": "./src/mitm/manager.stub.ts" } : {}; +} diff --git a/scripts/check/check-changelog-integrity.mjs b/scripts/check/check-changelog-integrity.mjs new file mode 100644 index 0000000000..b07c3f99f4 --- /dev/null +++ b/scripts/check/check-changelog-integrity.mjs @@ -0,0 +1,121 @@ +#!/usr/bin/env node +// scripts/check/check-changelog-integrity.mjs +// +// Anti "CHANGELOG-eat" gate: no bullet line that exists in the BASE branch's +// CHANGELOG.md may disappear in the merge result. The chronic failure mode is +// git's merge auto-resolve silently dropping sibling bullets (or whole version +// sections) when two branches touch adjacent CHANGELOG lines — incident +// 2026-07-05: PR #6193's merge ate 212 lines (the entire [3.8.45] + [3.8.44] +// sections, 130 bullets), only recovered by hand from the pre-merge ref. +// +// On pull_request CI the checkout is refs/pull/N/merge — the auto-resolved +// merge result — so comparing it against origin/ catches the eat BEFORE +// the merge lands, in the PR that would cause it. +// +// Policy (Princípio Zero): this only ever ADDS work for the maintainer side — +// quality.yml runs it blocking for own-origin PRs and report-only for forks. +// The release captain's reconciliation rewrites the CHANGELOG legitimately, +// but that happens on the release PR (PR → main, ci.yml), which does not run +// this gate. Escape hatch for intentional removals (e.g. reverting a reverted +// feature's bullet): ALLOW_CHANGELOG_REMOVALS=1 turns failures into a report. +// +// Usage: +// node scripts/check/check-changelog-integrity.mjs +// env GITHUB_BASE_REF PR base branch (CI); local fallback: current release/* +// env CHANGELOG_BASE_REF explicit ref override (e.g. origin/release/v3.8.45) +// env ALLOW_CHANGELOG_REMOVALS=1 report-only (never fails) + +import { execFileSync } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", ".."); +const CHANGELOG = "CHANGELOG.md"; + +/** Extract the set of bullet lines (trimmed) from a CHANGELOG text. */ +export function extractBullets(text) { + const bullets = new Set(); + for (const raw of String(text || "").split("\n")) { + const line = raw.trim(); + if (line.startsWith("- ") && line.length > 4) bullets.add(line); + } + return bullets; +} + +/** + * Bullet lines present in the base CHANGELOG but absent from the head + * CHANGELOG — the "eaten" set. Pure so it has a unit test. + */ +export function findLostBullets(baseText, headText) { + const headBullets = extractBullets(headText); + const lost = []; + for (const b of extractBullets(baseText)) { + if (!headBullets.has(b)) lost.push(b); + } + return lost; +} + +function git(args) { + return execFileSync("git", args, { cwd: ROOT, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 }); +} + +function resolveBaseRef() { + if (process.env.CHANGELOG_BASE_REF) return process.env.CHANGELOG_BASE_REF; + if (process.env.GITHUB_BASE_REF) return `origin/${process.env.GITHUB_BASE_REF}`; + // Local fallback: the highest release/v* on origin (the active development base). + try { + const branches = git(["branch", "-r", "--list", "origin/release/v*", "--format=%(refname:short)"]) + .split("\n") + .map((s) => s.trim()) + .filter(Boolean) + .sort((a, b) => a.localeCompare(b, undefined, { numeric: true })); + return branches[branches.length - 1] || null; + } catch { + return null; + } +} + +function main() { + const baseRef = resolveBaseRef(); + if (!baseRef) { + console.log("[changelog-integrity] SKIP — could not resolve a base ref (offline/fresh clone)."); + return 0; + } + + let baseText; + try { + baseText = git(["show", `${baseRef}:${CHANGELOG}`]); + } catch { + console.log(`[changelog-integrity] SKIP — ${CHANGELOG} not readable at ${baseRef}.`); + return 0; + } + const headText = readFileSync(join(ROOT, CHANGELOG), "utf8"); + + const lost = findLostBullets(baseText, headText); + if (lost.length === 0) { + console.log(`[changelog-integrity] OK — no base bullets lost vs ${baseRef}.`); + return 0; + } + + console.error( + `[changelog-integrity] ${lost.length} bullet(s) present in ${baseRef} are MISSING from this tree's ${CHANGELOG}:` + ); + for (const b of lost.slice(0, 15)) console.error(` ✗ ${b.slice(0, 160)}`); + if (lost.length > 15) console.error(` … and ${lost.length - 15} more`); + console.error( + "\nThis is the CHANGELOG-eat pattern (merge auto-resolve dropping sibling bullets)." + + "\nFix: restore the base CHANGELOG (`git checkout -- CHANGELOG.md`), re-insert ONLY" + + "\nyour own bullet, and prove the net diff is additive. Intentional removals (rare):" + + "\nre-run with ALLOW_CHANGELOG_REMOVALS=1 and justify in the PR body." + ); + if (process.env.ALLOW_CHANGELOG_REMOVALS === "1") { + console.error("[changelog-integrity] ALLOW_CHANGELOG_REMOVALS=1 — reporting only, not failing."); + return 0; + } + return 1; +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + process.exit(main()); +} diff --git a/scripts/check/check-test-discovery.mjs b/scripts/check/check-test-discovery.mjs index 144157122b..299e38b207 100644 --- a/scripts/check/check-test-discovery.mjs +++ b/scripts/check/check-test-discovery.mjs @@ -67,6 +67,11 @@ export const COLLECTORS = [ // 24.18 do CI; no 24.16 local vira um crawl de ~60s/arquivo). O resto da suíte roda // sob tsx/esm (~-50% de bootstrap por processo). Plano mestre testes+CI, QW-b. { glob: "tests/unit/dashboard/**/*.test.ts", sources: ["package.json"] }, + // Quarentena de flakes de concorrência (plano melhorias v3.8.46, P0.3): arquivos + // sensíveis a contenção de CPU/timing (classe glm-3580 / quota-division / + // provider-health-autopilot) rodam num passo dedicado --test-concurrency=1 ao FIM + // de cada runner. Fora dos globs paralelos acima por diretório próprio. + { glob: "tests/unit/serial/**/*.test.ts", sources: ["package.json"] }, // Órfãos religados (plano mestre QW-c): arquivos .test.mjs (top-level + db/ + feature-triage/) — fora do glob // *.test.ts histórico, nunca rodava em job nenhum (53 casos recuperados). { glob: "tests/unit/**/*.test.mjs", sources: ["package.json"] }, diff --git a/scripts/check/check-test-masking.mjs b/scripts/check/check-test-masking.mjs index 47b0060a58..1a2dddf95f 100644 --- a/scripts/check/check-test-masking.mjs +++ b/scripts/check/check-test-masking.mjs @@ -15,6 +15,8 @@ import { execFileSync } from "node:child_process"; import { pathToFileURL } from "node:url"; const TEST_RE = /\.(test|spec)\.(ts|tsx)$/; +// Production TypeScript sources (excludes test files, handled separately via TEST_RE). +const PROD_SRC_RE = /\.(ts|tsx|mts|cts)$/; /** Conta chamadas de assert.*( / assert( / expect( . */ export function countAssertions(src) { @@ -57,6 +59,169 @@ export function countExtendedTautologies(src) { return count; } +// ─── (6348) Subcheck 4: inline-reimplemented prod conditions (REPORT-ONLY) ─── +// A test that copies a conditional expression out of production code (instead of +// importing and exercising the symbol that owns it) is the wrong-shape-contract-test +// class (#6216): the assertion re-encodes the branch locally, so it stays green even +// when the real prod condition drifts. This subcheck is a HEURISTIC, textual gate +// mirroring the count* helpers above — it never parses an AST. It is REPORT-ONLY for +// now (warns, does not fail the gate). + +/** Collapse all runs of whitespace to a single space and trim. */ +function normalizeWhitespace(s) { + return (s || "").replace(/\s+/g, " ").trim(); +} + +/** + * Count "significant" tokens in a normalized condition. Single-char identifiers + * (`x`, `i`) and single-digit numeric literals (`0`, `1`) are treated as noise and + * NOT counted; operators and multi-char identifiers/numbers ARE. This is what makes + * `x > 0` trivial (1 significant token) while `status >= 500` is meaningful (3): + * - `status >= 500` → status, >=, 500 → 3 + * - `x === LIMIT && y` → ===, LIMIT, && → 3 + * - `x > 0` → > → 1 + */ +export function countSignificantTokens(cond) { + const tokens = + (cond || "").match( + /===|!==|==|!=|>=|<=|&&|\|\||[<>+\-*/%!]|[A-Za-z_$][\w$]*|\d+(?:\.\d+)?/g + ) || []; + let count = 0; + for (const tk of tokens) { + if (/^[A-Za-z_$]/.test(tk)) { + if (tk.length >= 2) count++; // multi-char identifier + } else if (/^\d/.test(tk)) { + if (tk.length >= 2) count++; // multi-digit number + } else { + count++; // operator + } + } + return count; +} + +/** A condition is "meaningful" when it carries ≥3 significant tokens. */ +function isSignificantCondition(cond) { + return countSignificantTokens(cond) >= 3; +} + +/** + * Extract meaningful (≥3-token) conditional expressions from a production source, + * paired with the nearest enclosing declared symbol that "owns" them. Covers + * `if (...)` (via paren balancing) and comparison-bearing ternaries (`a === b ? … : …`). + * Returns [{ condition (whitespace-normalized), owner }]. + */ +export function extractProdConditions(src) { + const results = []; + if (!src) return results; + + // Declarations (function / const / let / var) with their positions, so each + // condition can be attributed to the symbol whose body it lives in. + const decls = []; + const declRe = + /(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)|(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=/g; + let dm; + while ((dm = declRe.exec(src))) { + decls.push({ index: dm.index, name: dm[1] || dm[2] }); + } + const ownerAt = (idx) => { + let owner = ""; + for (const d of decls) { + if (d.index <= idx) owner = d.name; + else break; + } + return owner; + }; + + const seen = new Set(); + const pushCond = (raw, owner) => { + const norm = normalizeWhitespace(raw); + if (!norm || seen.has(norm) || !isSignificantCondition(norm)) return; + seen.add(norm); + results.push({ condition: norm, owner }); + }; + + // if (...) — balance parentheses to capture the full condition. + const ifRe = /\bif\s*\(/g; + let m; + while ((m = ifRe.exec(src))) { + let depth = 1; + let i = m.index + m[0].length; + for (; i < src.length && depth > 0; i++) { + const ch = src[i]; + if (ch === "(") depth++; + else if (ch === ")") depth--; + } + pushCond(src.slice(m.index + m[0].length, i - 1), ownerAt(m.index)); + } + + // Comparison-bearing ternaries: ` ? … : …` (best-effort, low-noise). + const ternRe = + /([A-Za-z_$][\w$).\]]*\s*(?:===|!==|==|!=|>=|<=|>|<)\s*[^?;{}\n]+?)\s*\?/g; + let t; + while ((t = ternRe.exec(src))) { + pushCond(t[1], ownerAt(t.index)); + } + + return results; +} + +/** + * Collect the identifiers/module specifiers a test file imports, so we can tell + * whether it exercises a prod symbol through the real import (clean) or merely + * re-implements one of its conditions locally (masked). Returns a Set of names: + * imported bindings, module paths, and module basenames. + */ +export function extractImports(src) { + const names = new Set(); + if (!src) return names; + const addModule = (mod) => { + names.add(mod); + const base = mod.split("/").pop().replace(/\.\w+$/, ""); + if (base) names.add(base); + }; + let m; + const importRe = /import\s+(?:type\s+)?([^;]*?)\s+from\s+['"]([^'"]+)['"]/g; + while ((m = importRe.exec(src))) { + addModule(m[2]); + for (const id of m[1].match(/[A-Za-z_$][\w$]*/g) || []) { + if (id !== "as" && id !== "type") names.add(id); + } + } + const dynRe = /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g; + while ((m = dynRe.exec(src))) addModule(m[1]); + const reqRe = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g; + while ((m = reqRe.exec(src))) addModule(m[1]); + return names; +} + +/** + * PURE core of subcheck 4. Given the sources of the prod files changed in a PR, + * one test file's source, and the set of names that test imports: return the prod + * conditions the test re-implements textually WITHOUT importing the symbol that owns + * them. Whitespace is squashed on both sides so spacing differences never mask a hit. + * Returns [{ condition, owner }]. + */ +export function findReimplementedConditions(prodSources, testSource, testImports) { + const flags = []; + if (!testSource) return flags; + const imports = + testImports instanceof Set ? testImports : new Set(testImports || []); + const squash = (s) => (s || "").replace(/\s+/g, ""); + const testSq = squash(testSource); + const seen = new Set(); + for (const prod of prodSources || []) { + for (const { condition, owner } of extractProdConditions(prod)) { + if (owner && imports.has(owner)) continue; // exercised through the real import + if (seen.has(condition)) continue; + if (testSq.includes(squash(condition))) { + seen.add(condition); + flags.push({ condition, owner: owner || null }); + } + } + } + return flags; +} + /** * (6A.10 subcheck 1) Sinaliza arquivos de teste DELETADOS ou renomeados-e-não- * substituídos. Recebe lista de paths de arquivos de teste que foram deletados @@ -241,14 +406,59 @@ function main() { // Only exempts the reduction signal; tautology/skip/deletion signals still fire. let assertReductionAllowlist = new Set(); let deletionAllowlist = {}; + let reimplementedAllowlist = new Set(); try { const raw = JSON.parse(fs.readFileSync("config/quality/test-masking-allowlist.json", "utf8")); assertReductionAllowlist = new Set(Object.keys(raw).filter((k) => !k.startsWith("_"))); deletionAllowlist = raw._deletedWithReplacement || {}; + reimplementedAllowlist = new Set(raw._reimplementedConditions || []); } catch { // no allowlist file — treat as empty } + // (6348 subcheck 4, REPORT-ONLY) Tests that inline-reimplement a prod condition + // instead of importing the symbol that owns it. Prod files changed in this PR + // (added/copied/modified TS sources) are the reference corpus; each changed test + // file is scanned against them. Warns only — it never fails the gate for now. + const prodChanged = git(["diff", "--name-only", "--diff-filter=ACM", `${base}...HEAD`]) + .split("\n") + .map((s) => s.trim()) + .filter((f) => PROD_SRC_RE.test(f) && !TEST_RE.test(f) && fs.existsSync(f)); + const prodSources = prodChanged.map((f) => { + try { + return fs.readFileSync(f, "utf8"); + } catch { + return ""; + } + }); + const changedTests = git(["diff", "--name-only", "--diff-filter=ACM", `${base}...HEAD`]) + .split("\n") + .map((s) => s.trim()) + .filter((f) => TEST_RE.test(f) && fs.existsSync(f)); + const reimplementedFlags = []; + if (prodSources.length) { + for (const tf of changedTests) { + if (reimplementedAllowlist.has(tf)) continue; + const src = fs.readFileSync(tf, "utf8"); + for (const hit of findReimplementedConditions(prodSources, src, extractImports(src))) { + reimplementedFlags.push( + `${tf}: re-implementa a condição \`${hit.condition}\`` + + (hit.owner ? ` (dona: ${hit.owner})` : "") + + " — asserte através do import real em vez de copiar a condição" + ); + } + } + } + if (reimplementedFlags.length) { + console.warn( + `[test-masking] (report-only) ${reimplementedFlags.length} teste(s) re-implementam ` + + `condição de produção em vez de importar o símbolo dono (classe #6216):\n` + + reimplementedFlags.map((f) => " ⚠ " + f).join("\n") + + `\n → importe o símbolo/função dono e asserte através dele (evita contrato duplicado ` + + `que diverge silenciosamente). Report-only por enquanto — não falha o gate.` + ); + } + const deletedFlags = evaluateDeletedFiles( [...deletedTests, ...relocatedOutOfTest], deletionAllowlist diff --git a/scripts/dev/http-method-guard.cjs b/scripts/dev/http-method-guard.cjs index 1c4586e3bf..e91b99557b 100644 --- a/scripts/dev/http-method-guard.cjs +++ b/scripts/dev/http-method-guard.cjs @@ -7,6 +7,7 @@ const HIGH_RISK_METHOD_RULES = [ [/^\/api\/auth\/logout\/?$/, ["POST"]], [/^\/api\/keys\/?$/, ["GET", "POST"]], [/^\/api\/keys\/[^/]+\/?$/, ["GET", "PATCH", "DELETE"]], + [/^\/api\/keys\/[^/]+\/devices\/?$/, ["GET"]], ]; let installed = false; diff --git a/scripts/dev/run-next-playwright.mjs b/scripts/dev/run-next-playwright.mjs index e4e800f26f..37a7055e8c 100644 --- a/scripts/dev/run-next-playwright.mjs +++ b/scripts/dev/run-next-playwright.mjs @@ -187,7 +187,8 @@ const testServerEnv = { }; export function shouldUseWebpackForPlaywrightDev({ mode, env }) { - return mode === "dev" && env.OMNIROUTE_USE_TURBOPACK !== "1"; + // Webpack only on the explicit escape hatch (=0) — turbopack is the default. + return mode === "dev" && env.OMNIROUTE_USE_TURBOPACK === "0"; } function runChild(command, args, env) { diff --git a/scripts/dev/run-next.mjs b/scripts/dev/run-next.mjs index 5b8a9af0ce..4a402650a8 100644 --- a/scripts/dev/run-next.mjs +++ b/scripts/dev/run-next.mjs @@ -11,6 +11,10 @@ import { createResponsesWsProxy } from "./responses-ws-proxy.mjs"; import { ensurePeerStampToken, stampPeerIp } from "./peer-stamp.mjs"; import methodGuard from "./http-method-guard.cjs"; import { ensureNativeSqlite } from "./ensure-native-sqlite.mjs"; +import { + isTurbopackCacheCorruption, + purgeAllTurbopackCaches, +} from "./turbopackCacheHeal.mjs"; import { randomUUID } from "node:crypto"; const { maybeHandleDisallowedMethod } = methodGuard; @@ -68,7 +72,10 @@ process.env.NODE_ENV = dev ? "development" : "production"; const { dashboardPort } = runtimePorts; const hostname = process.env.HOST || "0.0.0.0"; -const useTurbopack = dev && mergedEnv.OMNIROUTE_USE_TURBOPACK === "1"; +// Turbopack by default in dev (matches the Next 16 CLI default and the production +// build default in build-next-isolated.mjs); OMNIROUTE_USE_TURBOPACK=0 is the +// webpack escape hatch. +const useTurbopack = dev && mergedEnv.OMNIROUTE_USE_TURBOPACK !== "0"; process.env.OMNIROUTE_WS_BRIDGE_SECRET ||= randomUUID(); // Per-process secret used to prove the trusted peer-IP stamp came from this // server (read by the authz middleware in the same process). See peer-stamp.mjs. @@ -85,17 +92,46 @@ ensurePeerStampToken(); if (!useTurbopack) { delete process.env.TURBOPACK; } -const nextApp = next({ - dev, - dir: process.cwd(), - hostname, - port: dashboardPort, - turbopack: useTurbopack, - webpack: !useTurbopack, -}); +function createNextApp() { + return next({ + dev, + dir: process.cwd(), + hostname, + port: dashboardPort, + turbopack: useTurbopack, + webpack: !useTurbopack, + }); +} + +let nextApp = createNextApp(); + +// Best-effort self-heal for a corrupted Turbopack persistent dev cache (#6289): +// on Windows an mmap of an SST cache file can fail ("os error 1455" / paging +// file too small), which Turbopack surfaces as a misleading module-resolve +// error. This is an UPSTREAM Turbopack cache-corruption bug — not our code. +// When `prepare()` rejects with that signature we purge the cache and retry +// ONCE. Caveat: the failure often surfaces as a runtime overlay rather than a +// `prepare()` rejection, so this cannot always intercept it — the reliable +// remedy remains manually deleting `.build/next/**/cache/turbopack`. +async function prepareWithHeal() { + try { + await nextApp.prepare(); + } catch (error) { + const detail = error instanceof Error ? `${error.message}\n${error.stack ?? ""}` : String(error); + if (!useTurbopack || !isTurbopackCacheCorruption(detail)) throw error; + console.warn( + "[Next] Turbopack dev cache looks corrupted (Windows mmap / os error 1455 — known upstream bug). Purging and retrying once…" + ); + const removed = purgeAllTurbopackCaches(); + for (const dir of removed) console.warn(`[Next] purged Turbopack cache: ${dir}`); + nextApp = createNextApp(); + await nextApp.prepare(); + console.warn("[Next] Turbopack dev cache purged; startup retry succeeded."); + } +} async function start() { - await nextApp.prepare(); + await prepareWithHeal(); const requestHandler = nextApp.getRequestHandler(); const upgradeHandler = nextApp.getUpgradeHandler(); diff --git a/scripts/dev/turbopackCacheHeal.mjs b/scripts/dev/turbopackCacheHeal.mjs new file mode 100644 index 0000000000..7b6c2a110f --- /dev/null +++ b/scripts/dev/turbopackCacheHeal.mjs @@ -0,0 +1,74 @@ +// Best-effort self-heal for a corrupted Turbopack persistent dev cache. +// +// Context (#6289): on Windows, `pnpm dev` can fail at startup when Turbopack +// mmaps a persistent-cache SST file and the OS refuses the mapping +// ("os error 1455" — "paging file too small"). Turbopack then surfaces a +// misleading `Module not found: Can't resolve '@/shared/utils/machine'`. +// This is a known UPSTREAM Turbopack cache-corruption bug — NOT our code. +// The reliable remedy is deleting the Turbopack cache dir; this module lets +// the dev launcher attempt that automatically once before giving up. +// +// Pure + side-effect-isolated so it can be unit-tested without booting Next. + +import fs from "node:fs"; +import path from "node:path"; + +// Signature of the corrupted-cache failure. Kept intentionally broad because +// the same corruption surfaces through several messages (the raw mmap/SST +// error, the Windows paging-file error code, and the misleading module-resolve +// error emitted by Turbopack's "restore task data" step). +const CORRUPTION_SIGNATURE = /restore task data|mmap .*SST|os error 1455|paging file/i; + +/** + * True when an error message looks like a corrupted Turbopack persistent cache. + * @param {unknown} message + * @returns {boolean} + */ +export function isTurbopackCacheCorruption(message) { + if (message == null) return false; + return CORRUPTION_SIGNATURE.test(String(message)); +} + +/** + * Candidate Turbopack cache directories for a given Next dist dir. Next has + * placed the persistent cache under both `/cache/turbopack` and + * `/dev/cache/turbopack` across versions, so purge both. + * @param {string} [distDir] + * @param {string} [cwd] + * @returns {string[]} + */ +export function turbopackCacheDirs( + distDir = process.env.NEXT_DIST_DIR || ".build/next", + cwd = process.cwd() +) { + const base = path.isAbsolute(distDir) ? distDir : path.join(cwd, distDir); + return [ + path.join(base, "cache", "turbopack"), + path.join(base, "dev", "cache", "turbopack"), + ]; +} + +/** + * Recursively remove a single Turbopack cache directory. + * @param {string} dir + * @returns {boolean} true if the directory existed and was removed + */ +export function purgeTurbopackCache(dir) { + if (!dir || !fs.existsSync(dir)) return false; + fs.rmSync(dir, { recursive: true, force: true }); + return true; +} + +/** + * Purge every candidate Turbopack cache directory for the given dist dir. + * @param {string} [distDir] + * @param {string} [cwd] + * @returns {string[]} the directories that existed and were removed + */ +export function purgeAllTurbopackCaches(distDir, cwd) { + const removed = []; + for (const dir of turbopackCacheDirs(distDir, cwd)) { + if (purgeTurbopackCache(dir)) removed.push(dir); + } + return removed; +} diff --git a/scripts/quality/build-test-impact-map.mjs b/scripts/quality/build-test-impact-map.mjs index 1704c3a356..223d23bf2b 100644 --- a/scripts/quality/build-test-impact-map.mjs +++ b/scripts/quality/build-test-impact-map.mjs @@ -61,6 +61,8 @@ const testFiles = globSync( [ "tests/unit/*.test.ts", "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,executors,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui}/**/*.test.ts", + // Quarentena serial (P0.3): também são node:test — a TIA precisa mapeá-los. + "tests/unit/serial/**/*.test.ts", ], { cwd: ROOT, absolute: true } ); diff --git a/scripts/quality/validate-release-green.mjs b/scripts/quality/validate-release-green.mjs index 78091ce8ab..0b97f661d7 100644 --- a/scripts/quality/validate-release-green.mjs +++ b/scripts/quality/validate-release-green.mjs @@ -33,14 +33,21 @@ // orchestration lives in the /green-prs + review-prs flows that call it. // // Usage: -// node scripts/quality/validate-release-green.mjs [--json] [--with-build] [--quick] +// node scripts/quality/validate-release-green.mjs [--json] [--with-build] [--quick] [--hermetic] // --json emit machine-readable JSON to stdout (report goes to stderr) // --with-build also run check:pack-artifact (needs a dist/ build — slow) // --quick skip the slow unit + vitest + integration suites (drift + fast // gates only) +// --hermetic scrub OMNIROUTE_API_KEY/OMNIROUTE_URL from gate env so live +// tests self-skip exactly like CI (dev machines otherwise run +// them against localhost and produce false-positive reds) +// +// Per-gate output is saved to _artifacts/release-green/.log (gitignored) — +// diagnose a red from the file instead of re-running the gate. -import { execFileSync } from "node:child_process"; -import { readFileSync } from "node:fs"; +import { execFile, execFileSync } from "node:child_process"; +import { promisify } from "node:util"; +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -48,12 +55,27 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); const ROOT = join(__dirname, "..", ".."); const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm"; +// Per-gate captured output. execFileSync buffers everything and the report only +// shows a one-line summary, so without these files every red requires RE-RUNNING +// the gate just to see the detail (the dominant cost of the 2026-07-05 pre-flight). +const LOG_DIR = join(ROOT, "_artifacts", "release-green"); +function saveGateLog(id, out) { + try { + mkdirSync(LOG_DIR, { recursive: true }); + writeFileSync(join(LOG_DIR, `${id}.log`), String(out ?? "")); + } catch { + /* log persistence is best-effort — never fails a gate */ + } +} + // ─── Pure helpers (exported for tests) ────────────────────────────────────── /** Read the committed ratchet baseline value for a metric (null if unknown). */ export function baselineValue(metric, root = ROOT) { try { - const raw = JSON.parse(readFileSync(join(root, "config/quality/quality-baseline.json"), "utf8")); + const raw = JSON.parse( + readFileSync(join(root, "config/quality/quality-baseline.json"), "utf8") + ); const metrics = raw.metrics || raw; const v = metrics?.[metric]?.value; return typeof v === "number" ? v : null; @@ -139,6 +161,17 @@ export function classifyRunError(err, timeoutMs) { }; } +// --hermetic: scrub the live-test trigger vars so the pre-flight behaves like CI +// (a dev machine with OMNIROUTE_API_KEY set runs 17+ live tests that CI skips — +// every one a false-positive red against the release branch). +const HERMETIC_SCRUB = ["OMNIROUTE_API_KEY", "OMNIROUTE_URL"]; +let hermetic = false; +function buildGateEnv(extra) { + const env = { ...process.env, FORCE_COLOR: "0", ...(extra || {}) }; + if (hermetic) for (const k of HERMETIC_SCRUB) delete env[k]; + return env; +} + function run(cmd, cmdArgs, opts = {}) { try { const out = execFileSync(cmd, cmdArgs, { @@ -146,7 +179,7 @@ function run(cmd, cmdArgs, opts = {}) { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: 256 * 1024 * 1024, - env: { ...process.env, FORCE_COLOR: "0", ...(opts.env || {}) }, + env: buildGateEnv(opts.env), // A hard ceiling for the long, silent test suites (execFileSync buffers all output until // exit, so they show no progress while running). undefined = no timeout for fast gates. ...(opts.timeout ? { timeout: opts.timeout } : {}), @@ -157,11 +190,34 @@ function run(cmd, cmdArgs, opts = {}) { } } -function main() { +const execFileAsync = promisify(execFile); + +// Async twin of run() — same {code, out} contract, so the slow suites (unit / +// vitest / integration / pack-artifact) can run CONCURRENTLY instead of in +// series. Sequentially they dominate the pre-flight wall time (~2h in the +// v3.8.45 run); they are independent processes with per-process DATA_DIR +// isolation, so overlapping them cuts the pre-flight to ~the slowest single one. +async function runAsync(cmd, cmdArgs, opts = {}) { + try { + const { stdout, stderr } = await execFileAsync(cmd, cmdArgs, { + cwd: ROOT, + encoding: "utf8", + maxBuffer: 256 * 1024 * 1024, + env: buildGateEnv(opts.env), + ...(opts.timeout ? { timeout: opts.timeout } : {}), + }); + return { code: 0, out: `${stdout || ""}${stderr || ""}` }; + } catch (err) { + return classifyRunError(err, opts.timeout); + } +} + +async function main() { const args = new Set(process.argv.slice(2)); const JSON_OUT = args.has("--json"); const WITH_BUILD = args.has("--with-build"); const QUICK = args.has("--quick"); + hermetic = args.has("--hermetic"); const results = []; const record = (r) => { @@ -178,7 +234,14 @@ function main() { const hardCmd = (id, label, cmd, cmdArgs, opts) => { announce(label); const { code, out } = run(cmd, cmdArgs, opts); - record({ id, label, kind: "hard", ok: code === 0, detail: code === 0 ? "pass" : firstFailureLine(out) }); + saveGateLog(id, out); + record({ + id, + label, + kind: "hard", + ok: code === 0, + detail: code === 0 ? "pass" : firstFailureLine(out), + }); }; // A ratchet command (check:complexity, check:dead-code, …) exits 1 ONLY on a @@ -189,7 +252,14 @@ function main() { const driftCmd = (id, label, cmd, cmdArgs, okDetail = "within baseline", opts) => { announce(label); const { code, out } = run(cmd, cmdArgs, opts); - record({ id, label, kind: "drift", ok: code === 0, detail: code === 0 ? okDetail : firstFailureLine(out) }); + saveGateLog(id, out); + record({ + id, + label, + kind: "drift", + ok: code === 0, + detail: code === 0 ? okDetail : firstFailureLine(out), + }); }; process.stderr.write("🔎 Release-green validation (current working tree)\n\n"); @@ -198,14 +268,42 @@ function main() { // ESLint: ONE pass → errors (hard) + warnings (drift) { - announce("ESLint (errors + warnings — ~3-6min)"); - const { out } = run("npx", ["eslint", ".", "--format", "json"], { timeout: 15 * 60 * 1000 }); + announce("ESLint (errors + warnings — ~5-15min)"); + // Suppressions-aware, matching `npm run lint` (Pacote 4 no-new-warnings): the frozen + // pre-existing debt in config/quality/eslint-suppressions.json must not count as + // errors here — only NET-NEW violations are release reds. Timeout raised: a full + // repo pass takes ~14min alone and this pre-flight often runs alongside test suites. + const { out } = run( + "npx", + [ + "eslint", + ".", + "--format", + "json", + "--suppressions-location", + "config/quality/eslint-suppressions.json", + ], + { timeout: 30 * 60 * 1000 } + ); + saveGateLog("lint", out); const parsed = parseEslintJson(out); if (!parsed) { - record({ id: "lint", label: "ESLint", kind: "hard", ok: false, detail: "could not parse eslint json" }); + record({ + id: "lint", + label: "ESLint", + kind: "hard", + ok: false, + detail: "could not parse eslint json", + }); } else { const { errors, warnings } = eslintCounts(parsed); - record({ id: "lint-errors", label: "ESLint errors", kind: "hard", ok: errors === 0, detail: `${errors} error(s)` }); + record({ + id: "lint-errors", + label: "ESLint errors", + kind: "hard", + ok: errors === 0, + detail: `${errors} error(s)`, + }); const base = baselineValue("eslintWarnings"); const over = isDrift(warnings, base); record({ @@ -228,6 +326,7 @@ function main() { { announce("Cognitive complexity (ratchet)"); const { out } = run(npmCmd, ["run", "check:cognitive-complexity"]); + saveGateLog("cognitive", out); const current = parseCognitiveCount(out); const base = baselineValue("cognitiveComplexity"); const over = isDrift(current, base); @@ -267,6 +366,7 @@ function main() { const { code, out } = run(npmCmd, ["run", "check:test-masking"], { env: { GITHUB_BASE_REF: "main" }, }); + saveGateLog("test-masking", out); record({ id: "test-masking", label: "Test-masking (weakened-assert guard)", @@ -283,9 +383,20 @@ function main() { driftCmd("complexity", "Cyclomatic complexity (ratchet)", npmCmd, ["run", "check:complexity"]); driftCmd("dead-code", "Dead-code (ratchet)", npmCmd, ["run", "check:dead-code"]); driftCmd("type-coverage", "Type coverage (ratchet)", npmCmd, ["run", "check:type-coverage"]); - driftCmd("compression-budget", "Compression budget (ratchet)", npmCmd, ["run", "check:compression-budget"]); - driftCmd("openapi-coverage", "OpenAPI route coverage (ratchet)", npmCmd, ["run", "check:openapi-coverage"]); - driftCmd("workflow-lint", "Workflow lint (zizmor ratchet)", npmCmd, ["run", "check:workflows", "--", "--ratchet"]); + driftCmd("compression-budget", "Compression budget (ratchet)", npmCmd, [ + "run", + "check:compression-budget", + ]); + driftCmd("openapi-coverage", "OpenAPI route coverage (ratchet)", npmCmd, [ + "run", + "check:openapi-coverage", + ]); + driftCmd("workflow-lint", "Workflow lint (zizmor ratchet)", npmCmd, [ + "run", + "check:workflows", + "--", + "--ratchet", + ]); driftCmd("codeql-ratchet", "CodeQL alerts (ratchet)", npmCmd, ["run", "check:codeql-ratchet"]); // Docs sync + fabricated-docs (strict) is a real-defect gate (invented env vars / @@ -298,15 +409,69 @@ function main() { // with 15 such reds). They run SILENTLY for many minutes; the announce line above + these // hard ceilings keep a long-but-healthy run from being mistaken for a hang (the ceiling also // converts a genuine DB-handle hang into a visible failure instead of an infinite block). - hardCmd("unit", "Unit tests (full suite, CI concurrency — runs ~20-35min silently)", npmCmd, ["run", "test:unit:ci"], { timeout: 45 * 60 * 1000 }); - hardCmd("vitest", "Vitest (MCP / autoCombo / cache — ~3-8min)", npmCmd, ["run", "test:vitest"], { timeout: 15 * 60 * 1000 }); - // Integration tests run ONLY on the release PR full CI (PR→main), so an assertion - // regression here (e.g. a contributor flipping a Codex fingerprint key order) is - // invisible until release — run them in the pre-flight as a HARD gate. - hardCmd("integration", "Integration tests (~3-10min)", npmCmd, ["run", "test:integration"], { timeout: 20 * 60 * 1000 }); - } - if (WITH_BUILD) { - hardCmd("pack-artifact", "Package artifact (npm pack policy)", npmCmd, ["run", "check:pack-artifact"], { timeout: 20 * 60 * 1000 }); + // The slow suites are INDEPENDENT processes (each self-isolates DATA_DIR) with + // no shared state, so they run CONCURRENTLY — the pre-flight wall time becomes + // ~the slowest single suite instead of their sum (unit ~25-35min + vitest + // ~3-8min + integration ~3-10min + pack-artifact ~15min was ~1h serial in the + // v3.8.45 run). pack-artifact (--with-build) joins the same wave. Integration + // runs ONLY on the release PR full CI, so a regression here is invisible until + // release — that is why it is a HARD pre-flight gate. + const slow = [ + { + id: "unit", + label: "Unit tests (full suite, CI concurrency — runs ~20-35min silently)", + args: ["run", "test:unit:ci"], + timeout: 45 * 60 * 1000, + }, + { + id: "vitest", + label: "Vitest (MCP / autoCombo / cache — ~3-8min)", + args: ["run", "test:vitest"], + timeout: 15 * 60 * 1000, + }, + { + id: "integration", + label: "Integration tests (~3-10min)", + args: ["run", "test:integration"], + timeout: 20 * 60 * 1000, + }, + ]; + if (WITH_BUILD) { + slow.push({ + id: "pack-artifact", + label: "Package artifact (npm pack policy)", + args: ["run", "check:pack-artifact"], + timeout: 20 * 60 * 1000, + }); + } + slow.forEach((g) => announce(`${g.label} [parallel]`)); + const slowResults = await Promise.all( + slow.map((g) => runAsync(npmCmd, g.args, { timeout: g.timeout })) + ); + slow.forEach((g, i) => { + const { code, out } = slowResults[i]; + saveGateLog(g.id, out); + record({ + id: g.id, + label: g.label, + kind: "hard", + ok: code === 0, + detail: code === 0 ? "pass" : firstFailureLine(out), + }); + }); + } else if (WITH_BUILD) { + // --with-build without the suites (--quick): still verify the package artifact. + const { code, out } = await runAsync(npmCmd, ["run", "check:pack-artifact"], { + timeout: 20 * 60 * 1000, + }); + saveGateLog("pack-artifact", out); + record({ + id: "pack-artifact", + label: "Package artifact (npm pack policy)", + kind: "hard", + ok: code === 0, + detail: code === 0 ? "pass" : firstFailureLine(out), + }); } const { releaseGreen, hardFailures, drift } = computeVerdict(results); diff --git a/scripts/release/sync-next-cycle.mjs b/scripts/release/sync-next-cycle.mjs index c7867843ae..3a223baad3 100644 --- a/scripts/release/sync-next-cycle.mjs +++ b/scripts/release/sync-next-cycle.mjs @@ -64,6 +64,18 @@ export function insertNextSection(mainChangelog, nextSection, nextVersion) { .replace(/\n{4,}/g, "\n\n\n"); } +/** Pure: the version whose `## [x.y.z]` heading follows `version`'s section (or null). */ +export function versionAfter(changelog, version) { + const lines = changelog.split("\n"); + const start = lines.findIndex((l) => l.startsWith(`## [${version}]`)); + if (start === -1) return null; + for (let i = start + 1; i < lines.length; i++) { + const m = lines[i].match(/^## \[(\d+\.\d+\.\d+)\]/); + if (m) return m[1]; + } + return null; +} + /** Pure: extract the `## []` section (header included, next heading excluded). */ export function extractSection(changelog, version) { const lines = changelog.split("\n"); @@ -89,7 +101,9 @@ export function extractSection(changelog, version) { } function git(args, opts = {}) { - return execFileSync("git", args, { encoding: "utf8", ...opts }).trim(); + // maxBuffer: the default 1 MiB overflows on `git show origin/main:CHANGELOG.md` + // (the CHANGELOG alone is >1 MiB) — ENOBUFS found live in the v3.8.45 run (2026-07-06). + return execFileSync("git", args, { encoding: "utf8", maxBuffer: 64 * 1024 * 1024, ...opts }).trim(); } function main() { @@ -163,6 +177,18 @@ function main() { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], }); + // Also propagate the just-FINALIZED [prevVersion] section (dated bullets + + // Contributors) into the mirrors — syncing only [NEXT] leaves the shipped + // section as "— TBD" in all 42 mirrors (found live in the v3.8.45 run). + // Boundary = the version heading right below it in main's CHANGELOG. + const belowPrev = versionAfter(mainChangelog, prevVersion); + if (belowPrev) { + execFileSync("npm", ["run", "release:sync-changelog-i18n", "--", prevVersion, belowPrev], { + cwd: WT, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }); + } execFileSync("git", ["add", "-A", "docs/i18n"], { cwd: WT }); } catch (e) { console.warn("[sync-next-cycle] i18n mirror resync failed (resolve manually):", e.message); diff --git a/scripts/vps/release-runner-down.sh b/scripts/vps/release-runner-down.sh new file mode 100755 index 0000000000..dd27087cad --- /dev/null +++ b/scripts/vps/release-runner-down.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Desliga a VM self-hosted (VPS 113) ao fim do release e volta o CI para o GitHub-hosted. +# Idempotente: seguro chamar mesmo que a VM já esteja desligada. +# +# Uso: scripts/vps/release-runner-down.sh +set -uo pipefail + +PVE_HOST="${PVE_HOST:-192.168.0.100}" +VM_ID="${VM_ID:-113}" +REPO="${REPO:-diegosouzapw/OmniRoute}" +SSH="ssh -o BatchMode=yes -o ConnectTimeout=8" + +# 1) Volta o CI para ubuntu-latest ANTES de derrubar a VM (evita jobs presos). +echo "[release-runner] USE_VPS_RUNNER=false (CI volta ao GitHub-hosted)." +gh variable set USE_VPS_RUNNER --repo "$REPO" --body "false" >/dev/null 2>&1 || true + +# 2) Shutdown graceful da VM (libera os 32 cores / 24GB de volta ao host). +echo "[release-runner] desligando VM $VM_ID (graceful)..." +$SSH "root@$PVE_HOST" "qm shutdown $VM_ID --timeout 120" 2>/dev/null \ + || $SSH "root@$PVE_HOST" "qm stop $VM_ID" 2>/dev/null \ + || echo "[release-runner] ⚠️ não consegui desligar a VM $VM_ID — verifique manualmente." +echo "[release-runner] pronto." diff --git a/scripts/vps/release-runner-up.sh b/scripts/vps/release-runner-up.sh new file mode 100755 index 0000000000..8f35066d33 --- /dev/null +++ b/scripts/vps/release-runner-up.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# Liga a VM self-hosted (VPS 113) e aguarda seus runners ficarem online, para a fase +# de release usar runners dedicados (anti-fila). Falha => o caller cai no GitHub-hosted. +# +# Uso: scripts/vps/release-runner-up.sh [timeout_seg] +# Saída: exit 0 + seta a repo-var USE_VPS_RUNNER=true quando >=1 runner omni-release online +# exit 1 + seta USE_VPS_RUNNER=false em qualquer falha/timeout (fallback) +# +# Pré-requisitos no host que roda o /generate-release: +# - chave SSH autorizada em root@$PVE_HOST (Proxmox) e root@$VPS_HOST (a VM) +# - gh autenticado com admin no repo (para ler runners + setar a variable) +set -uo pipefail + +PVE_HOST="${PVE_HOST:-192.168.0.100}" # Proxmox host +VPS_HOST="${VPS_HOST:-192.168.0.113}" # a VM dos runners +VM_ID="${VM_ID:-113}" +REPO="${REPO:-diegosouzapw/OmniRoute}" +LABEL="${RUNNER_LABEL:-omni-release}" +TIMEOUT="${1:-120}" +SSH="ssh -o BatchMode=yes -o ConnectTimeout=8" + +fallback() { + echo "[release-runner] ⚠️ $1 — usando GitHub-hosted (fallback)." + gh variable set USE_VPS_RUNNER --repo "$REPO" --body "false" >/dev/null 2>&1 || true + exit 1 +} + +echo "[release-runner] ligando VM $VM_ID no Proxmox $PVE_HOST..." +$SSH "root@$PVE_HOST" "qm start $VM_ID" 2>/dev/null || true # ok se já estiver rodando + +echo "[release-runner] aguardando runners '$LABEL' ficarem online (timeout ${TIMEOUT}s)..." +deadline=$(( $(date +%s) + TIMEOUT )) +while [ "$(date +%s)" -lt "$deadline" ]; do + online=$(gh api "repos/$REPO/actions/runners" \ + --jq "[.runners[] | select(.status==\"online\") | select(.labels[].name==\"$LABEL\")] | length" \ + 2>/dev/null || echo 0) + if [ "${online:-0}" -ge 1 ]; then + echo "[release-runner] ✅ $online runner(s) '$LABEL' online — usando a VPS." + gh variable set USE_VPS_RUNNER --repo "$REPO" --body "true" >/dev/null 2>&1 \ + || fallback "não consegui setar USE_VPS_RUNNER" + exit 0 + fi + sleep 6 +done +fallback "runners não ficaram online a tempo" diff --git a/skills/cli-contexts/SKILL.md b/skills/cli-contexts/SKILL.md index f7faefa731..c25640c92f 100644 --- a/skills/cli-contexts/SKILL.md +++ b/skills/cli-contexts/SKILL.md @@ -227,6 +227,9 @@ Add a new context - `--api-key ` - `--api-key-stdin` +- `--access-token ` +- `--access-token-stdin` +- `--scope ` - `--description ` **Example:** @@ -247,7 +250,11 @@ omniroute contexts use ### `contexts current` -Show current active context name +Show the active context (server, auth, scope) + +**Flags:** + +- `--name-only` **Example:** diff --git a/skills/cli-serve/SKILL.md b/skills/cli-serve/SKILL.md index 2818910229..19dfd0ddc4 100644 --- a/skills/cli-serve/SKILL.md +++ b/skills/cli-serve/SKILL.md @@ -2,7 +2,6 @@ name: cli-serve description: Start, stop, and restart the OmniRoute server from the CLI. Manage daemon mode, port configuration, auto-recovery, system tray integration, and the dashboard open shortcut. --- - ## Overview @@ -56,6 +55,8 @@ omniroute restart - `--max-restarts ` - `--tray` - `--no-tray` +- `--tls-cert ` +- `--tls-key ` **Example:** diff --git a/skills/cli-setup/SKILL.md b/skills/cli-setup/SKILL.md index 34c0a62892..4df7cb49bb 100644 --- a/skills/cli-setup/SKILL.md +++ b/skills/cli-setup/SKILL.md @@ -41,6 +41,14 @@ omniroute autostart enable omniroute autostart disable ``` +### `autostart toggle` + +**Example:** + +```bash +omniroute autostart toggle +``` + ### `autostart status` **Example:** diff --git a/skills/config-codex-cli/SKILL.md b/skills/config-codex-cli/SKILL.md index 8c2daf46c9..2b57a62a17 100644 --- a/skills/config-codex-cli/SKILL.md +++ b/skills/config-codex-cli/SKILL.md @@ -1,369 +1,20 @@ --- name: config-codex-cli -description: Step-by-step agent workflow to configure the OpenAI Codex CLI on any machine (Linux, macOS, Windows) to use OmniRoute as backend. Detects OS and shell, writes config.toml and 7 named profiles, sets environment variables, and verifies the setup. +description: Step-by-step agent workflow to configure the OpenAI Codex CLI on any machine (Linux, macOS, Windows) to use OmniRoute as an OpenAI-compatible backend. Detects OS and shell, writes config.toml and 7 named profiles, sets environment variables, and verifies the setup. --- + -# /config-codex-cli — Codex CLI Configuration Workflow +## Overview -Configure the Codex CLI on this machine to use an OmniRoute instance as backend. +Step-by-step agent workflow to configure the OpenAI Codex CLI on any machine (Linux, macOS, Windows) to use OmniRoute as an OpenAI-compatible backend. Detects OS and shell, writes config.toml and 7 named profiles, sets environment variables, and verifies the setup. -After this skill completes, `codex` will use OmniRoute by default with `cx/gpt-5.5` (xhigh reasoning), 7 named profiles for quick switching, and proper token limits configured for each model. - ---- - -## Step 0 — Collect required inputs from the user - -Before doing anything, ask the user for the two required values. Do not proceed until both are provided: - -1. **OmniRoute host** — the IP or hostname of the OmniRoute server (e.g. `192.168.0.1`, `100.x.x.x` for Tailscale, or `localhost`) -2. **OmniRoute API key** — the API key for OmniRoute (starts with `sk-`) - -Store these as local variables for the rest of the skill: -- `OMNI_HOST` = the host the user provided (no trailing slash, no port — port 20128 is appended by this skill) -- `OMNI_KEY` = the API key - ---- - -## Step 1 — Detect environment - -Run the following to gather machine facts. Store results — they are used in later steps. +## Quick install ```bash -# OS detection -uname -s # Linux / Darwin (macOS) / MINGW*/CYGWIN*/MSYS* = Windows/Git Bash - -# Home directory -echo $HOME # Linux / macOS / Git Bash -echo $USERPROFILE # Windows native (PowerShell / cmd) - -# Current shell -echo $SHELL # Linux / macOS: /bin/bash, /bin/zsh, /bin/fish, etc. - -# Shell profile file -# Resolve which file to append env vars to: -# bash → ~/.bashrc (Linux) or ~/.bash_profile (macOS) -# zsh → ~/.zshrc -# fish → ~/.config/fish/config.fish -# PowerShell (Windows) → $PROFILE (run: echo $PROFILE inside PowerShell) - -# PATH and common tool directories (to populate shell_environment_policy later) -echo $PATH -which node 2>/dev/null && node --version -echo ${NVM_DIR:-not-set} -echo ${BUN_INSTALL:-not-set} -echo ${SDKMAN_DIR:-not-set} -echo ${JAVA_HOME:-not-set} +npm install -g omniroute # or: npx omniroute +omniroute --version ``` -Based on the OS result, set the **Codex config directory**: -- Linux / macOS / Git Bash: `~/.codex/` -- Windows native (PowerShell): `$env:USERPROFILE\.codex\` +## Subcommands ---- - -## Step 2 — Verify Codex CLI is installed - -```bash -codex --version -``` - -If this fails, stop and tell the user to install the Codex CLI first: - -```bash -npm install -g @openai/codex -``` - -Then re-run the skill from Step 1. - ---- - -## Step 3 — Create the Codex config directory - -```bash -mkdir -p ~/.codex # Linux / macOS -# Windows PowerShell: New-Item -ItemType Directory -Force "$env:USERPROFILE\.codex" -``` - ---- - -## Step 4 — Write `~/.codex/config.toml` - -Read the existing file first if it exists (to avoid overwriting MCP server entries, skills, projects, or notify configurations the user may already have). - -If the file **does not exist**: create it with the full content below. - -If the file **already exists**: apply only the fields listed in the "Model/Inference", "Behaviour", "Auth/Credentials", "Features", "TUI", "Notice", and "Model providers" sections — do **not** remove existing `[mcp_servers.*]`, `[projects.*]`, `[[skills.config]]`, or `notify` entries. - -Replace `` with the value collected in Step 0. - -**Content to write (or merge):** - -```toml -# ── Model / Inference ───────────────────────────────────────────────────────── -model = "cx/gpt-5.5" -model_provider = "omniroute" -model_reasoning_effort = "xhigh" -model_reasoning_summary = "detailed" -model_verbosity = "high" -model_context_window = 400000 -model_auto_compact_token_limit = 350000 -model_max_output_tokens = 65536 -tool_output_token_limit = 32768 - -# ── Behaviour ───────────────────────────────────────────────────────────────── -approval_policy = "never" -sandbox_mode = "danger-full-access" -personality = "pragmatic" -web_search = "live" -check_for_update_on_startup = true - -# ── Auth / Credentials ──────────────────────────────────────────────────────── -cli_auth_credentials_store = "file" -mcp_oauth_credentials_store = "file" - -# ── Features ────────────────────────────────────────────────────────────────── -[features] -shell_snapshot = true -unified_exec = true -multi_agent = true -memories = true -js_repl = true -apps = false -terminal_resize_reflow = true - -# ── TUI ─────────────────────────────────────────────────────────────────────── -[tui] -theme = "dracula" -status_line = ["model-with-reasoning", "current-dir", "context-remaining", "context-used", "five-hour-limit"] - -[tui.model_availability_nux] -"gpt-5.5" = 4 - -# ── Shell environment passed into sandboxed commands ────────────────────────── -# Populate [shell_environment_policy.set] with the PATH and tool dirs discovered -# in Step 1. Only include paths that actually exist on this machine. -# Minimum required: SHELL. -[shell_environment_policy] -inherit = "all" -experimental_use_profile = true - -[shell_environment_policy.set] -SHELL = "" -# Add any of the following that exist on this machine: -# PATH = "" -# NVM_DIR = "" -# BUN_INSTALL = "" -# SDKMAN_DIR = "" -# JAVA_HOME = "" - -# ── Notice / UI flags ───────────────────────────────────────────────────────── -[notice] -hide_full_access_warning = true -hide_rate_limit_model_nudge = true -fast_default_opt_out = true - -# ── Model providers ─────────────────────────────────────────────────────────── -# env_key = NAME of the environment variable (not the value). -# The actual key is stored in the shell profile (Step 5), never here. -[model_providers.omniroute] -name = "OmniRoute" -base_url = "http://:20128/v1" -env_key = "OMNIROUTE_API_KEY" -requires_openai_auth = false -wire_api = "responses" -``` - -> **TOML rule:** `[[skills.config]]` array-of-tables must be the **last** section in the file. If the file already has `[[skills.config]]` entries, keep them at the end after inserting the new provider block. - ---- - -## Step 5 — Write profile files - -> **Naming rule (Codex CLI v0.137+):** files must be `~/.codex/.config.toml` — **no `profile-` prefix**. The CLI resolves `-p chat` to `~/.codex/chat.config.toml`. If the file is not found, the default applies silently with no error. - -Create each file below in the Codex config directory (`~/.codex/`). If a file already exists, overwrite it. - -### `chat.config.toml` — no reasoning (server default = medium) - -```toml -model = "cx/gpt-5.5" -model_provider = "omniroute" -``` - -### `low.config.toml` - -```toml -model = "cx/gpt-5.5" -model_reasoning_effort = "low" -model_provider = "omniroute" -``` - -### `medium.config.toml` - -```toml -model = "cx/gpt-5.5" -model_reasoning_effort = "medium" -model_provider = "omniroute" -``` - -### `high.config.toml` - -```toml -model = "cx/gpt-5.5" -model_reasoning_effort = "high" -model_provider = "omniroute" -``` - -### `xhigh.config.toml` - -```toml -model = "cx/gpt-5.5" -model_reasoning_effort = "xhigh" -model_provider = "omniroute" -``` - -### `deepseek.config.toml` — DeepSeek V4 Pro, 1M context - -```toml -model = "ds/deepseek-v4-pro" -model_provider = "omniroute" - -model_context_window = 1000000 -model_auto_compact_token_limit = 900000 -model_max_output_tokens = 65536 -tool_output_token_limit = 65536 -``` - -### `mistral.config.toml` — Mistral Large Latest, 256k context - -```toml -model = "mistral/mistral-large-latest" -model_provider = "omniroute" - -model_context_window = 262144 -model_auto_compact_token_limit = 220000 -model_max_output_tokens = 32768 -tool_output_token_limit = 16384 -``` - ---- - -## Step 6 — Set environment variables in the shell profile - -Determine the correct shell profile file from Step 1, then append the following block **only if the variables are not already present**. - -Before writing, check: -```bash -grep -l "OMNIROUTE_API_KEY" ~/.bashrc ~/.zshrc ~/.bash_profile 2>/dev/null -``` - -If the variable already exists in any profile file, update the value in-place instead of appending a duplicate. - -**Block to append (replace `` with the key collected in Step 0):** - -```bash -# OmniRoute API key — used by Codex CLI (env_key = "OMNIROUTE_API_KEY" in config) -export OMNIROUTE_API_KEY="" - -# Codex CLI / Claude Code output cap (64k — covers any file or diff a coding assistant generates) -export CLAUDE_CODE_MAX_OUTPUT_TOKENS=65536 -``` - -**Shell profile file by OS/shell:** - -| OS / Shell | Profile file | -|------------|-------------| -| Linux — bash | `~/.bashrc` | -| Linux — zsh | `~/.zshrc` | -| macOS — zsh (default) | `~/.zshrc` | -| macOS — bash | `~/.bash_profile` | -| Linux/macOS — fish | `~/.config/fish/config.fish` (use `set -Ux` syntax instead of `export`) | -| Windows — PowerShell | `$PROFILE` (run `echo $PROFILE` in PowerShell to get the path) | -| Windows — Git Bash | `~/.bashrc` | - -**fish syntax:** - -```fish -set -Ux OMNIROUTE_API_KEY "" -set -Ux CLAUDE_CODE_MAX_OUTPUT_TOKENS 65536 -``` - -**PowerShell syntax:** - -```powershell -[System.Environment]::SetEnvironmentVariable("OMNIROUTE_API_KEY", "", "User") -[System.Environment]::SetEnvironmentVariable("CLAUDE_CODE_MAX_OUTPUT_TOKENS", "65536", "User") -``` - ---- - -## Step 7 — Apply and verify - -```bash -# Apply the shell profile (Linux/macOS) -source ~/.bashrc # or ~/.zshrc depending on Step 1 - -# Verify variables are set -echo $OMNIROUTE_API_KEY # must print the key -echo $CLAUDE_CODE_MAX_OUTPUT_TOKENS # must print 65536 - -# Verify Codex picks up the provider -codex config get model_provider # must print: omniroute - -# Smoke test — must return a response without auth errors -codex -p chat "reply with only the word OK" -``` - -If the smoke test returns an authentication error: -- Re-check that `source ~/.bashrc` was run (or open a new terminal) -- Run `curl http://:20128/v1/models` to confirm OmniRoute is reachable -- Confirm the key is correct with `echo $OMNIROUTE_API_KEY` - ---- - -## Reference — profiles quick table - -| Profile | Command | Best for | -|---------|---------|----------| -| `chat` | `codex -p chat "..."` | Explain, light questions | -| `low` | `codex -p low "..."` | Rename, format, trivial edits | -| `medium` | `codex -p medium "..."` | Debug, moderate refactor | -| `high` | `codex -p high "..."` | New features, complex tests | -| `xhigh` | *(default)* | Architecture, deep analysis | -| `deepseek` | `codex -p deepseek "..."` | Long codebase analysis (1M context) | -| `mistral` | `codex -p mistral "..."` | Cost-conscious tasks | - -Per-invocation overrides (bypass profiles entirely): - -```bash -codex -m cx/gpt-5.5 -c model_reasoning_effort=low "rename var x to count" -codex -m ds/deepseek-v4-pro "analyze the entire repo" -``` - ---- - -## Reference — why `wire_api = "responses"` works for all models - -Codex CLI deprecated `wire_api = "chat"` in February 2026. OmniRoute bridges the gap transparently: - -``` -Codex CLI → POST /v1/responses → OmniRoute → POST /chat/completions → DeepSeek / Mistral / any provider -``` - -All profiles use the same `wire_api = "responses"`. OmniRoute handles translation for every upstream provider. - ---- - -## Reference — token fields - -| Field | Controls | -|-------|----------| -| `model_context_window` | Total token budget | -| `model_auto_compact_token_limit` | Compaction trigger (max 90% of context window) | -| `model_max_output_tokens` | Max tokens per API response (sent on every request) | -| `tool_output_token_limit` | Max tokens stored per tool call in session history | -| `CLAUDE_CODE_MAX_OUTPUT_TOKENS` | Same concept for the Claude Code CLI | - ---- - -*Full reference: `docs/guides/CODEX-CLI-CONFIGURATION.md` in the OmniRoute repository.* +_No CLI subcommands mapped for this family yet._ diff --git a/skills/omni-api-keys/SKILL.md b/skills/omni-api-keys/SKILL.md index a2563b0ea2..7e6169879d 100644 --- a/skills/omni-api-keys/SKILL.md +++ b/skills/omni-api-keys/SKILL.md @@ -34,6 +34,26 @@ curl -X POST https://localhost:20128/api/keys \ -d '{}' ``` +### GET /api/keys/{id} + +Get API key + +```bash +curl https://localhost:20128/api/keys/{id} \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" +``` + +### PATCH /api/keys/{id} + +Update API key + +```bash +curl -X PATCH https://localhost:20128/api/keys/{id} \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + ### DELETE /api/keys/{id} Delete API key @@ -43,6 +63,17 @@ curl -X DELETE https://localhost:20128/api/keys/{id} \ -H "Authorization: Bearer $OMNIROUTE_TOKEN" ``` +### GET /api/keys/{id}/devices + +List devices for an API key + +Lists the distinct devices (masked IP + User-Agent fingerprints) tracked for an API key by the in-memory device tracker. IPs are masked before storage; the route never sees the raw client IP. + +```bash +curl https://localhost:20128/api/keys/{id}/devices \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" +``` + ## Payloads See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas. diff --git a/skills/omni-cli-tools/SKILL.md b/skills/omni-cli-tools/SKILL.md index ff468040af..76c3e54b23 100644 --- a/skills/omni-cli-tools/SKILL.md +++ b/skills/omni-cli-tools/SKILL.md @@ -315,6 +315,76 @@ curl -X DELETE https://localhost:20128/api/cli-tools/openclaw-settings \ -H "Authorization: Bearer $OMNIROUTE_TOKEN" ``` +### GET /api/cli-tools/crush-settings + +Read Crush CLI OmniRoute config + +Local-only. Reads the OmniRoute provider block in Crush's config. + +```bash +curl https://localhost:20128/api/cli-tools/crush-settings \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" +``` + +### POST /api/cli-tools/crush-settings + +Write Crush CLI OmniRoute config + +Local-only. Registers OmniRoute as an `openai-compat` provider in Crush's config. + +```bash +curl -X POST https://localhost:20128/api/cli-tools/crush-settings \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + +### DELETE /api/cli-tools/crush-settings + +Remove OmniRoute from Crush CLI config + +Local-only. Removes the OmniRoute provider block from Crush's config. + +```bash +curl -X DELETE https://localhost:20128/api/cli-tools/crush-settings \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" +``` + +### GET /api/cli-tools/codewhale-settings + +Read CodeWhale CLI OmniRoute config + +Local-only. Reads the OmniRoute config block from `~/.codewhale/config.toml` (with `~/.deepseek/config.toml` legacy fallback). + +```bash +curl https://localhost:20128/api/cli-tools/codewhale-settings \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" +``` + +### POST /api/cli-tools/codewhale-settings + +Write CodeWhale CLI OmniRoute config + +Local-only. Writes the OmniRoute config block in CodeWhale TOML format. + +```bash +curl -X POST https://localhost:20128/api/cli-tools/codewhale-settings \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + +### DELETE /api/cli-tools/codewhale-settings + +Remove OmniRoute from CodeWhale CLI config + +Local-only. Removes the OmniRoute config block from CodeWhale's config. + +```bash +curl -X DELETE https://localhost:20128/api/cli-tools/codewhale-settings \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" +``` + ## Payloads See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas. diff --git a/skills/omni-github-skills/SKILL.md b/skills/omni-github-skills/SKILL.md new file mode 100644 index 0000000000..355bfe6d61 --- /dev/null +++ b/skills/omni-github-skills/SKILL.md @@ -0,0 +1,20 @@ +--- +name: omni-github-skills +description: Search, score, scan, and import agent skills from GitHub repositories that contain SKILL.md, CLAUDE.md, .cursorrules, and similar agent skill files. Discover community skills across 160+ provider categories, evaluate relevance with heuristic scoring, check for malware or hardcoded secrets, and install into Hermes, Claude Code, Gemini CLI, or OpenCode agent directories. +--- + + +## Overview + +Search, score, scan, and import agent skills from GitHub repositories that contain SKILL.md, CLAUDE.md, .cursorrules, and similar agent skill files. Discover community skills across 160+ provider categories, evaluate relevance with heuristic scoring, check for malware or hardcoded secrets, and install into Hermes, Claude Code, Gemini CLI, or OpenCode agent directories. + +## Authentication + +All requests require a valid Bearer token or session cookie. Obtain a token via `POST /api/auth/login` or configure `REQUIRE_API_KEY=false` for local development. + +## Endpoints + +_No endpoints mapped for this area yet._ +## Payloads + +See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas. diff --git a/skills/omni-inference/SKILL.md b/skills/omni-inference/SKILL.md index 8c9e0e0a42..40079c16f9 100644 --- a/skills/omni-inference/SKILL.md +++ b/skills/omni-inference/SKILL.md @@ -27,6 +27,17 @@ curl -X POST https://localhost:20128/api/v1/chat/completions \ -d '{}' ``` +### GET /api/v1/ws + +Chat completion over WebSocket (handshake + upgrade) + +OpenAI-compatible chat over a WebSocket connection. `GET` with `?handshake=1` returns the connection descriptor (auth path, message protocol and live-event channels) as JSON; a plain `GET` without an Upgrade returns `426 Upgrade Required`. After upgrading, the client exchanges JSON frames — `{type:"request", id, payload:{model, messages}}` to start a completion and `{type:"cancel", id}` to abort it. A separate live channel (default port `LIVE_WS_PORT=20129`, path `/live`) streams dashboard events on the `requests`, `combo` and `credentials` topics with a 15s heartbeat. Requires an API key. + +```bash +curl https://localhost:20128/api/v1/ws \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" +``` + ### POST /api/v1/providers/{provider}/chat/completions Create chat completion (provider-specific) @@ -208,6 +219,54 @@ curl https://localhost:20128/api/v1/providers/{provider}/models \ -H "Authorization: Bearer $OMNIROUTE_TOKEN" ``` +### POST /api/v1/ocr + +Document OCR + +Mistral OCR–compatible document OCR endpoint. Accepts a JSON body referencing a document/image and returns extracted text. Success responses carry the `X-OmniRoute-*` cost-telemetry headers. + +```bash +curl -X POST https://localhost:20128/api/v1/ocr \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + +### POST /api/v1/audio/translations + +Translate audio to English + +OpenAI Whisper–compatible audio translation (multipart/form-data). Unlike `/api/v1/audio/transcriptions`, output is always English regardless of the source language. Success responses carry the `X-OmniRoute-*` cost-telemetry headers. + +```bash +curl -X POST https://localhost:20128/api/v1/audio/translations \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + +### GET /api/v1/providers/suggested-models + +Suggested media models + +Read-only server-side proxy to the public HuggingFace Hub models search API, used by the dashboard to suggest models for a media provider kind without exposing an HF token client-side. Never accepts or returns credentials. + +```bash +curl https://localhost:20128/api/v1/providers/suggested-models \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" +``` + +### GET /api/v1/provider-plugin-manifest + +Provider plugin manifest + +Returns the manifest describing installed provider plugins. + +```bash +curl https://localhost:20128/api/v1/provider-plugin-manifest \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" +``` + ## Payloads See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas. diff --git a/skills/omni-providers/SKILL.md b/skills/omni-providers/SKILL.md index 1856ca0ff8..acda134239 100644 --- a/skills/omni-providers/SKILL.md +++ b/skills/omni-providers/SKILL.md @@ -114,6 +114,50 @@ curl https://localhost:20128/api/providers/client \ -H "Authorization: Bearer $OMNIROUTE_TOKEN" ``` +### POST /api/providers/agy-auth/import + +Import an Antigravity CLI (agy) token file as an `agy` connection + +```bash +curl -X POST https://localhost:20128/api/providers/agy-auth/import \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + +### POST /api/providers/agy-auth/import-bulk + +Bulk-import multiple Antigravity CLI (agy) token files (up to 50) + +```bash +curl -X POST https://localhost:20128/api/providers/agy-auth/import-bulk \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + +### POST /api/providers/agy-auth/zip-extract + +Extract `.json` token files from an uploaded ZIP for agy bulk import + +```bash +curl -X POST https://localhost:20128/api/providers/agy-auth/zip-extract \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + +### POST /api/providers/agy-auth/apply-local + +Auto-detect and import the local Antigravity CLI (agy) login from disk + +```bash +curl -X POST https://localhost:20128/api/providers/agy-auth/apply-local \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + ### GET /api/provider-nodes List provider nodes diff --git a/skills/omni-settings/SKILL.md b/skills/omni-settings/SKILL.md index fc9bcae210..55fae9a557 100644 --- a/skills/omni-settings/SKILL.md +++ b/skills/omni-settings/SKILL.md @@ -14,6 +14,102 @@ All requests require a valid Bearer token or session cookie. Obtain a token via ## Endpoints +### GET /api/settings/memory + +Get memory settings + +Returns the extended memory settings including 7 new fields added in plan 21 (embeddingSource, embeddingProviderModel, transformersEnabled, staticEnabled, rerankEnabled, rerankProviderModel, vectorStore). + +```bash +curl https://localhost:20128/api/settings/memory \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" +``` + +### PUT /api/settings/memory + +Update memory settings + +Update any subset of the extended memory settings. All fields are optional; only provided fields are updated. Schema: `MemorySettingsExtendedSchema`. + +```bash +curl -X PUT https://localhost:20128/api/settings/memory \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + +### GET /api/settings/qdrant + +Get Qdrant settings + +Returns current Qdrant configuration. The `apiKey` field is never returned raw — use `hasApiKey` / `apiKeyMasked` instead. + +```bash +curl https://localhost:20128/api/settings/qdrant \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" +``` + +### PUT /api/settings/qdrant + +Update Qdrant settings + +Update Qdrant configuration. Pass `apiKey: ""` to remove the stored key. Schema: `QdrantSettingsUpdateSchema`. + +```bash +curl -X PUT https://localhost:20128/api/settings/qdrant \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + +### GET /api/settings/qdrant/health + +Qdrant health probe + +Performs a liveness check against the configured Qdrant instance. Returns latency and any connection error (sanitized — no stack traces). + +```bash +curl https://localhost:20128/api/settings/qdrant/health \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" +``` + +### POST /api/settings/qdrant/search + +Qdrant semantic search test + +Performs a test semantic search against the Qdrant collection. Useful for validating that the integration works end-to-end. Schema: `QdrantSearchSchema`. + +```bash +curl -X POST https://localhost:20128/api/settings/qdrant/search \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + +### POST /api/settings/qdrant/cleanup + +Clean up expired Qdrant points + +Removes Qdrant points for memories that have expired or exceeded the configured retention window. + +```bash +curl -X POST https://localhost:20128/api/settings/qdrant/cleanup \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + +### GET /api/settings/qdrant/embedding-models + +List Qdrant embedding models + +Returns the list of embedding models available for use with Qdrant. + +```bash +curl https://localhost:20128/api/settings/qdrant/embedding-models \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" +``` + ### GET /api/settings Get application settings @@ -34,6 +130,19 @@ curl -X PATCH https://localhost:20128/api/settings \ -d '{}' ``` +### POST /api/settings/purge-request-history + +Clear request log history + +Deletes `call_logs`, legacy `request_detail_logs`, and local request artifact files under `DATA_DIR/call_logs`. + +```bash +curl -X POST https://localhost:20128/api/settings/purge-request-history \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + ### GET /api/settings/compression Get global compression settings @@ -54,6 +163,28 @@ curl -X PUT https://localhost:20128/api/settings/compression \ -d '{}' ``` +### GET /api/settings/compression/mcp-accessibility + +Get the MCP tool-output accessibility (trimming) config + +```bash +curl https://localhost:20128/api/settings/compression/mcp-accessibility \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" +``` + +### PUT /api/settings/compression/mcp-accessibility + +Update the MCP tool-output accessibility (trimming) config + +Partial-merge update. Numeric floors (e.g. a maxTextChars below the truncation-tail reserve) are folded back to the safe defaults server-side, so the response reflects the effective config. + +```bash +curl -X PUT https://localhost:20128/api/settings/compression/mcp-accessibility \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + ### GET /api/settings/payload-rules Get payload rules configuration @@ -217,6 +348,41 @@ curl https://localhost:20128/api/tags \ -H "Authorization: Bearer $OMNIROUTE_TOKEN" ``` +### GET /api/settings/quota-store + +Get current quota store driver settings + +Redis URL is masked in the response (shows only scheme+host). + +```bash +curl https://localhost:20128/api/settings/quota-store \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" +``` + +### PUT /api/settings/quota-store + +Update quota store driver settings + +```bash +curl -X PUT https://localhost:20128/api/settings/quota-store \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + +### POST /api/settings/purge-usage-history + +Purge usage history + +Dashboard-only. Purges stored usage-history records. + +```bash +curl -X POST https://localhost:20128/api/settings/purge-usage-history \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + ## Payloads See the full OpenAPI specification at `GET /api/openapi/spec` or `docs/openapi.yaml` for detailed request/response schemas. diff --git a/skills/omni-version-manager/SKILL.md b/skills/omni-version-manager/SKILL.md index 167b5815a7..85f21839c4 100644 --- a/skills/omni-version-manager/SKILL.md +++ b/skills/omni-version-manager/SKILL.md @@ -205,6 +205,184 @@ curl -X POST https://localhost:20128/api/services/cliproxy/auto-start \ -d '{}' ``` +### POST /api/services/mux/install + +Install Mux from npm + +Installs the `mux` npm package (coder/mux — local agent-orchestration daemon) under DATA_DIR/services/mux/. **LOCAL_ONLY** — loopback only. + +```bash +curl -X POST https://localhost:20128/api/services/mux/install \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + +### POST /api/services/mux/start + +Start Mux + +Spawns `mux server --host 127.0.0.1 --port `. Idempotent if already running. **LOCAL_ONLY** — loopback only. + +```bash +curl -X POST https://localhost:20128/api/services/mux/start \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + +### POST /api/services/mux/stop + +Stop Mux + +Gracefully stops Mux. Idempotent. **LOCAL_ONLY** — loopback only. + +```bash +curl -X POST https://localhost:20128/api/services/mux/stop \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + +### POST /api/services/mux/restart + +Restart Mux + +stop() then start() under the operation lock. **LOCAL_ONLY** — loopback only. + +```bash +curl -X POST https://localhost:20128/api/services/mux/restart \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + +### POST /api/services/mux/update + +Update Mux to a newer npm version + +Stops, installs newer version, restarts. **LOCAL_ONLY** — loopback only. + +```bash +curl -X POST https://localhost:20128/api/services/mux/update \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + +### GET /api/services/mux/status + +Get Mux status + +Returns live supervisor state and DB metadata. **LOCAL_ONLY** — loopback only. + +```bash +curl https://localhost:20128/api/services/mux/status \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" +``` + +### POST /api/services/mux/auto-start + +Toggle Mux auto-start + +When enabled, Mux starts automatically on the next OmniRoute boot. **LOCAL_ONLY** — loopback only. + +```bash +curl -X POST https://localhost:20128/api/services/mux/auto-start \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + +### POST /api/services/bifrost/install + +Install Bifrost + +Installs the `@maximhq/bifrost` npm package under DATA_DIR/services/bifrost/. The package downloads the Go binary on first run. Accepts an optional `version` field (semver or `latest`). **LOCAL_ONLY** — loopback only. + +```bash +curl -X POST https://localhost:20128/api/services/bifrost/install \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + +### POST /api/services/bifrost/start + +Start Bifrost + +Starts the supervised Bifrost process. **LOCAL_ONLY** — loopback only. + +```bash +curl -X POST https://localhost:20128/api/services/bifrost/start \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + +### POST /api/services/bifrost/stop + +Stop Bifrost + +Stops the supervised Bifrost process. **LOCAL_ONLY** — loopback only. + +```bash +curl -X POST https://localhost:20128/api/services/bifrost/stop \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + +### POST /api/services/bifrost/restart + +Restart Bifrost + +Restarts the supervised Bifrost process. **LOCAL_ONLY** — loopback only. + +```bash +curl -X POST https://localhost:20128/api/services/bifrost/restart \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + +### POST /api/services/bifrost/update + +Update Bifrost + +Updates Bifrost to the latest npm version. Stops the running process, installs the new version, and restarts if it was previously running. **LOCAL_ONLY** — loopback only. + +```bash +curl -X POST https://localhost:20128/api/services/bifrost/update \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + +### GET /api/services/bifrost/status + +Get Bifrost status + +Returns live and DB status for the supervised Bifrost service. **LOCAL_ONLY** — loopback only. + +```bash +curl https://localhost:20128/api/services/bifrost/status \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" +``` + +### POST /api/services/bifrost/auto-start + +Toggle Bifrost auto-start + +When enabled, Bifrost starts automatically on the next OmniRoute boot. **LOCAL_ONLY** — loopback only. + +```bash +curl -X POST https://localhost:20128/api/services/bifrost/auto-start \ + -H "Authorization: Bearer $OMNIROUTE_TOKEN" + -H "Content-Type: application/json" \ + -d '{}' +``` + ### GET /api/services/{name}/logs Stream service logs via SSE diff --git a/src/app/(dashboard)/dashboard/HomePageClient.tsx b/src/app/(dashboard)/dashboard/HomePageClient.tsx index 37db2baa6b..1f376c147a 100644 --- a/src/app/(dashboard)/dashboard/HomePageClient.tsx +++ b/src/app/(dashboard)/dashboard/HomePageClient.tsx @@ -112,6 +112,7 @@ export default function HomePageClient({ machineId }: HomePageClientProps) { const [baseUrl, setBaseUrl] = useState("/v1"); const [selectedProvider, setSelectedProvider] = useState(null); const [providerMetrics, setProviderMetrics] = useState>({}); + const [providerTopology, setProviderTopology] = useState({ lastProvider: "", errorProvider: "" }); const [providerNodes, setProviderNodes] = useState< Array<{ id?: string; prefix?: string; name?: string }> >([]); @@ -304,6 +305,10 @@ export default function HomePageClient({ machineId }: HomePageClientProps) { const data = await metricsRes.json(); if (!cancelled) { setProviderMetrics(data.metrics || {}); + setProviderTopology({ + lastProvider: normalizeProviderId(data.topology?.lastProvider), + errorProvider: normalizeProviderId(data.topology?.errorProvider), + }); } } } catch (error) { @@ -494,28 +499,7 @@ export default function HomePageClient({ machineId }: HomePageClientProps) { return Array.from(byProvider.values()); }, [providerStats, providerMetrics, providerNodes]); - const { lastProvider, errorProvider } = useMemo(() => { - let recentProvider = ""; - let recentTimestamp = 0; - let recentErrorProvider = ""; - let recentErrorTimestamp = 0; - - for (const [provider, metrics] of Object.entries(providerMetrics)) { - const requestTimestamp = metrics.lastRequestAt ? Date.parse(metrics.lastRequestAt) : 0; - if (Number.isFinite(requestTimestamp) && requestTimestamp > recentTimestamp) { - recentProvider = normalizeProviderId(provider); - recentTimestamp = requestTimestamp; - } - - const errorTimestamp = metrics.lastErrorAt ? Date.parse(metrics.lastErrorAt) : 0; - if (Number.isFinite(errorTimestamp) && errorTimestamp > recentErrorTimestamp) { - recentErrorProvider = normalizeProviderId(provider); - recentErrorTimestamp = errorTimestamp; - } - } - - return { lastProvider: recentProvider, errorProvider: recentErrorProvider }; - }, [providerMetrics]); + const { lastProvider, errorProvider } = providerTopology; const pollBackgroundUpdate = useCallback( async ({ diff --git a/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx b/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx index 3a54c5a74f..e4a0de2e2e 100644 --- a/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx +++ b/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx @@ -203,6 +203,7 @@ export default function ApiManagerPageClient() { const createKeyFormRef = useRef(null); const [keys, setKeys] = useState([]); const [allModels, setAllModels] = useState([]); + const [modelsLoaded, setModelsLoaded] = useState(false); const [allCombos, setAllCombos] = useState([]); const [allConnections, setAllConnections] = useState([]); const [loading, setLoading] = useState(true); @@ -320,14 +321,40 @@ export default function ApiManagerPageClient() { }, [showAddModal, nameError, scrollCreateKeyFormToTop]); const fetchModels = async () => { + setModelsLoaded(false); try { const res = await fetch("/v1/models"); if (res.ok) { const data = await res.json(); - setAllModels(data.data || []); + setAllModels(Array.isArray(data.data) ? data.data : []); + return; + } + + // Fallback for dashboard API-key editing: /v1/models can be protected by + // API-key catalog auth, but the dashboard still needs a stable catalog so + // users can edit allowedModels. /api/models?all=true returns the static + // dashboard model inventory in a slightly different shape. + const fallbackRes = await fetch("/api/models?all=true"); + if (fallbackRes.ok) { + const fallbackData = await fallbackRes.json(); + const fallbackModels = Array.isArray(fallbackData.models) ? fallbackData.models : []; + setAllModels( + fallbackModels + .map((m: any) => ({ + id: typeof m.fullModel === "string" ? m.fullModel : `${m.provider}/${m.model}`, + owned_by: typeof m.provider === "string" ? m.provider : "unknown", + name: typeof m.alias === "string" ? m.alias : m.model || m.fullModel, + })) + .filter((m: Model) => typeof m.id === "string" && m.id.length > 0) + ); + } else { + setAllModels([]); } } catch (error) { console.log("Error fetching models:", error); + setAllModels([]); + } finally { + setModelsLoaded(true); } }; @@ -1533,6 +1560,7 @@ export default function ApiManagerPageClient() { apiKey={editingKey} modelsByProvider={filteredModelsByProvider} allModels={permissionModels} + modelsLoaded={modelsLoaded} allCombos={allCombos} allConnections={allConnections} searchModel={searchModel} @@ -1552,6 +1580,7 @@ const PermissionsModal = memo(function PermissionsModal({ apiKey, modelsByProvider, allModels, + modelsLoaded, allCombos, allConnections, searchModel, @@ -1563,6 +1592,7 @@ const PermissionsModal = memo(function PermissionsModal({ apiKey: ApiKey; modelsByProvider: ProviderGroup[]; allModels: Model[]; + modelsLoaded: boolean; allCombos: ComboOption[]; allConnections: ProviderConnection[]; searchModel: string; @@ -2024,7 +2054,13 @@ const PermissionsModal = memo(function PermissionsModal({ allowAll ? "text-green-700 dark:text-green-300" : "text-amber-700 dark:text-amber-300" }`} > - {allowAll ? t("allowAllDesc") : t("restrictDesc", { selectedCount, totalModels })} + {allowAll + ? t("allowAllDesc") + : !modelsLoaded + ? t("restrictLoading") + : totalModels === 0 + ? t("restrictCatalogUnavailable", { selectedCount }) + : t("restrictDesc", { selectedCount, totalModels })}

diff --git a/src/app/(dashboard)/dashboard/combos/WeightTotalBar.tsx b/src/app/(dashboard)/dashboard/combos/WeightTotalBar.tsx index 3d0a6cbe65..bacb890994 100644 --- a/src/app/(dashboard)/dashboard/combos/WeightTotalBar.tsx +++ b/src/app/(dashboard)/dashboard/combos/WeightTotalBar.tsx @@ -20,6 +20,20 @@ const WEIGHT_COLORS = [ "bg-indigo-500", ]; +/** + * #6147 — effective routing share of a weighted target. + * + * The per-target `weight` values do not have to sum to 100; at routing time each + * target is picked with probability `weight / Σweights`. So a raw weight of 30 + * with a total of 60 is an *effective* 50% share. This pure helper computes that + * share (0-100) and guards the `total === 0` case so the UI never renders NaN. + */ +export function effectiveSharePercent(weight: number, total: number): number { + if (!weight || weight <= 0) return 0; + if (!total || total <= 0) return 0; + return (weight / total) * 100; +} + export default function WeightTotalBar({ models }: WeightTotalBarProps) { const total = models.reduce((sum, m) => sum + (m.weight || 0), 0); const isValid = total === 100; @@ -49,6 +63,16 @@ export default function WeightTotalBar({ models }: WeightTotalBarProps) { className={`inline-block w-1.5 h-1.5 rounded-full ${WEIGHT_COLORS[i % WEIGHT_COLORS.length]}`} /> {m.weight}% + {/* #6147 — show the *effective* routing share when weights don't sum to 100 */} + {total > 0 && total !== 100 && ( + + {" → "} + {Math.round(effectiveSharePercent(m.weight, total))}% + + )} ) )} diff --git a/src/app/(dashboard)/dashboard/combos/page.tsx b/src/app/(dashboard)/dashboard/combos/page.tsx index 5f546db165..c5c068000c 100644 --- a/src/app/(dashboard)/dashboard/combos/page.tsx +++ b/src/app/(dashboard)/dashboard/combos/page.tsx @@ -3972,6 +3972,54 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, combo + {/* #6168: per-combo session-stickiness override (tri-state so it can + force ON or OFF regardless of the global default; blank = inherit). */} +
+ + +
{strategy === "context-relay" && (
diff --git a/src/app/(dashboard)/dashboard/free-provider-rankings/page.tsx b/src/app/(dashboard)/dashboard/free-provider-rankings/page.tsx index a112f299d5..a4689b1569 100644 --- a/src/app/(dashboard)/dashboard/free-provider-rankings/page.tsx +++ b/src/app/(dashboard)/dashboard/free-provider-rankings/page.tsx @@ -60,14 +60,21 @@ export default function FreeProviderRankingsPage() { const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [filter, setFilter] = useState(""); + const [configuredOnly, setConfiguredOnly] = useState(false); + const [availableOnly, setAvailableOnly] = useState(false); const fetchRankings = useCallback( - async (category?: string) => { + async (category?: string, opts?: { configuredOnly?: boolean; availableOnly?: boolean }) => { setLoading(true); setError(""); try { - const url = category - ? `/api/free-provider-rankings?category=${category}` + const params = new URLSearchParams(); + if (category) params.set("category", category); + if (opts?.configuredOnly) params.set("configuredOnly", "1"); + if (opts?.availableOnly) params.set("availableOnly", "1"); + const qs = params.toString(); + const url = qs + ? `/api/free-provider-rankings?${qs}` : "/api/free-provider-rankings"; const res = await fetch(url); if (!res.ok) throw new Error(`HTTP ${res.status}`); @@ -83,8 +90,8 @@ export default function FreeProviderRankingsPage() { ); useEffect(() => { - fetchRankings(filter || undefined); - }, [filter, fetchRankings]); + fetchRankings(filter || undefined, { configuredOnly, availableOnly }); + }, [filter, configuredOnly, availableOnly, fetchRankings]); return (
@@ -113,6 +120,33 @@ export default function FreeProviderRankingsPage() { ))}
+ {/* Availability toggles (default off → show all providers) */} +
+ + +
+ {error &&
{error}
} {loading ? ( diff --git a/src/app/(dashboard)/dashboard/playground/components/ParamSliders.tsx b/src/app/(dashboard)/dashboard/playground/components/ParamSliders.tsx index ebbd48883a..c13b60b269 100644 --- a/src/app/(dashboard)/dashboard/playground/components/ParamSliders.tsx +++ b/src/app/(dashboard)/dashboard/playground/components/ParamSliders.tsx @@ -11,6 +11,10 @@ export interface PlaygroundParams { seed: number | null; stop: string; jsonMode: boolean; + // #6241: canonical reasoning params — surfaced by ReasoningControls only when the selected + // model supports thinking. `effort: ""` means "not set" (left off the request body). + effort: string; + thinking: boolean; } export const DEFAULT_PARAMS: PlaygroundParams = { @@ -22,6 +26,8 @@ export const DEFAULT_PARAMS: PlaygroundParams = { seed: null, stop: "", jsonMode: false, + effort: "", + thinking: false, }; interface ParamSlidersProps { diff --git a/src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx b/src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx new file mode 100644 index 0000000000..85777f71c1 --- /dev/null +++ b/src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx @@ -0,0 +1,75 @@ +"use client"; + +// src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx +// +// #6241: effort selector + thinking toggle for the Playground. Rendered only when the selected +// model supports thinking (spec.show); the effort options come from the model's `effort_tiers` +// (fallback to the canonical vocabulary), resolved by `resolveReasoningControls`. + +import type { PlaygroundParams } from "./ParamSliders"; +import type { ReasoningControlSpec } from "./reasoningControls"; + +interface ReasoningControlsProps { + spec: ReasoningControlSpec; + params: PlaygroundParams; + setParams: (params: PlaygroundParams) => void; +} + +/** Human-friendly label for a canonical/tier effort value (e.g. "xhigh" -> "Xhigh"). */ +function effortLabel(value: string): string { + return value.charAt(0).toUpperCase() + value.slice(1); +} + +export default function ReasoningControls({ spec, params, setParams }: ReasoningControlsProps) { + if (!spec.show) return null; + + function update(key: K, value: PlaygroundParams[K]) { + setParams({ ...params, [key]: value }); + } + + return ( +
+ + Reasoning + + + {/* Thinking toggle */} +
+ + +
+ + {/* Effort selector */} +
+ + +
+
+ ); +} diff --git a/src/app/(dashboard)/dashboard/playground/components/StudioConfigPane.tsx b/src/app/(dashboard)/dashboard/playground/components/StudioConfigPane.tsx index 21ca3082c2..0efa69f6e0 100644 --- a/src/app/(dashboard)/dashboard/playground/components/StudioConfigPane.tsx +++ b/src/app/(dashboard)/dashboard/playground/components/StudioConfigPane.tsx @@ -16,6 +16,11 @@ import { OPENAI_COMPATIBLE_PREFIX, } from "@/shared/constants/providers"; import { pickDefaultModel, resolveModelFilterKey } from "./modelSelection"; +import ReasoningControls from "./ReasoningControls"; +import { + resolveReasoningControls, + type ReasoningControlSpec, +} from "./reasoningControls"; export interface ConfigState { endpoint: PlaygroundEndpoint; @@ -24,6 +29,10 @@ export interface ConfigState { provider?: string; systemPrompt: string; params: PlaygroundParams; + // #6241: resolved reasoning-control spec for the selected model (which controls to show + the + // effort tiers). Kept here so the tabs (ChatTab) can gate `effort`/`thinking` on the request + // body to models that actually support thinking. + reasoning?: ReasoningControlSpec; } interface StudioConfigPaneProps { @@ -79,7 +88,12 @@ export default function StudioConfigPane({ configState, setConfigState }: Studio selectedProviderOption?.modelPrefix, isCompatibleConnectionId ); - const { availableModels, loading: loadingModels } = useAvailableModels(modelFilterKey); + const { availableModels, modelCapabilities, loading: loadingModels } = + useAvailableModels(modelFilterKey); + + // #6241: resolve the reasoning controls for the currently selected model from the capability + // flags the /models catalog exposes (supportsThinking / effort_tiers). + const reasoningSpec = resolveReasoningControls(modelCapabilities[configState.model]); // #3731: selecting a provider resets the model to "", and nothing picked a default — // so the active model stayed empty and the chat failed with "Set a model". Auto-select @@ -90,6 +104,18 @@ export default function StudioConfigPane({ configState, setConfigState }: Studio // eslint-disable-next-line react-hooks/exhaustive-deps }, [availableModels, configState.model]); + // #6241: keep the resolved reasoning spec on configState so the tabs (ChatTab) can gate the + // `effort`/`thinking` request fields on models that support thinking. Sync only when it changes. + useEffect(() => { + const current = configState.reasoning; + const changed = + !current || + current.show !== reasoningSpec.show || + current.effortOptions.join(",") !== reasoningSpec.effortOptions.join(","); + if (changed) setConfigState({ ...configState, reasoning: reasoningSpec }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [reasoningSpec.show, reasoningSpec.effortOptions.join(",")]); + function update(key: K, value: ConfigState[K]) { setConfigState({ ...configState, [key]: value }); } @@ -227,6 +253,13 @@ export default function StudioConfigPane({ configState, setConfigState }: Studio update("params", p)} />
+ + {/* Reasoning controls — only shown when the selected model supports thinking (#6241) */} + update("params", p)} + /> ); diff --git a/src/app/(dashboard)/dashboard/playground/components/reasoningControls.ts b/src/app/(dashboard)/dashboard/playground/components/reasoningControls.ts new file mode 100644 index 0000000000..275ab206bf --- /dev/null +++ b/src/app/(dashboard)/dashboard/playground/components/reasoningControls.ts @@ -0,0 +1,75 @@ +// Playground reasoning-control helpers (#6241). +// +// The `/v1/models` catalog additively exposes per-model reasoning capability flags on each +// entry's `capabilities` object (see src/lib/modelMetadataRegistry.ts::enrichCatalogModelEntry): +// - `supportsThinking` (boolean) — whether the model exposes a thinking/reasoning mode. +// - `effort_tiers` (string[]) — the canonical effort vocabulary to offer, present only +// when the model supports thinking. +// These pure helpers turn that capability object into (a) a spec describing which controls the +// Playground should render + the effort options, and (b) the reasoning request fields to fold +// onto the chat request body — gated so `effort`/`thinking` are emitted ONLY when the selected +// model supports thinking and the chosen values are valid. Exported + unit-tested directly. + +import { CANONICAL_EFFORT_VALUES } from "@/shared/reasoning/effortStandardization"; + +/** Subset of a `/v1/models` entry's `capabilities` object the Playground reasoning UI reads. */ +export interface ModelReasoningCapabilities { + supportsThinking?: boolean | null; + effort_tiers?: unknown; +} + +/** What the Playground should render for the current model's reasoning support. */ +export interface ReasoningControlSpec { + /** Render the effort selector + thinking toggle only when true. */ + show: boolean; + /** Canonical effort tiers to offer in the selector (empty when `show` is false). */ + effortOptions: string[]; +} + +/** The subset of playground params the reasoning request-field builder consumes. */ +export interface ReasoningParams { + effort?: string; + thinking?: boolean; +} + +const HIDDEN_SPEC: ReasoningControlSpec = { show: false, effortOptions: [] }; + +/** + * Resolve which reasoning controls to render for a model's capability object. + * - Hidden (`show: false`) when the model does not support thinking (or caps is missing). + * - Otherwise `show: true` with the model's `effort_tiers`, falling back to the canonical + * effort vocabulary when the model omits or empties the tier list. + */ +export function resolveReasoningControls( + caps: ModelReasoningCapabilities | null | undefined +): ReasoningControlSpec { + if (!caps || caps.supportsThinking !== true) return HIDDEN_SPEC; + const tiers = Array.isArray(caps.effort_tiers) + ? caps.effort_tiers.filter((t): t is string => typeof t === "string" && t.length > 0) + : []; + return { + show: true, + effortOptions: tiers.length > 0 ? tiers : [...CANONICAL_EFFORT_VALUES], + }; +} + +/** + * Build the reasoning fields to fold onto the chat request body from the current params + the + * resolved control spec. Emits nothing when the model does not support thinking; includes + * `effort` only when it is set AND part of the offered tiers; includes `thinking` only when the + * toggle is on. This is the single "only when set/supported" gate for the request body. + */ +export function buildReasoningRequestFields( + params: ReasoningParams, + spec: ReasoningControlSpec +): { effort?: string; thinking?: boolean } { + if (!spec.show) return {}; + const out: { effort?: string; thinking?: boolean } = {}; + if (params.effort && spec.effortOptions.includes(params.effort)) { + out.effort = params.effort; + } + if (params.thinking) { + out.thinking = true; + } + return out; +} diff --git a/src/app/(dashboard)/dashboard/playground/components/tabs/ChatTab.tsx b/src/app/(dashboard)/dashboard/playground/components/tabs/ChatTab.tsx index 1dd6e6d551..8a0c3512f5 100644 --- a/src/app/(dashboard)/dashboard/playground/components/tabs/ChatTab.tsx +++ b/src/app/(dashboard)/dashboard/playground/components/tabs/ChatTab.tsx @@ -9,6 +9,7 @@ import { useStreamMetrics } from "../../hooks/useStreamMetrics"; import { getModelPricing } from "@/lib/playground/types"; import type { ConfigState } from "../StudioConfigPane"; import type { StreamMetrics } from "@/shared/schemas/playground"; +import { buildReasoningRequestFields } from "../reasoningControls"; interface Message { role: "system" | "user" | "assistant"; @@ -77,6 +78,16 @@ export default function ChatTab({ configState, onMetricsUpdate }: ChatTabProps) if (p.stop.trim()) body.stop = p.stop; if (p.jsonMode) body.response_format = { type: "json_object" }; + // #6241: fold the canonical effort / thinking params onto the body — gated to models that + // support thinking (via the resolved reasoning spec) and to valid effort tiers. + Object.assign( + body, + buildReasoningRequestFields( + { effort: p.effort, thinking: p.thinking }, + configState.reasoning ?? { show: false, effortOptions: [] } + ) + ); + return body; } diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/PassthroughModelsSection.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/PassthroughModelsSection.tsx index fb0d2b6e62..857281e3bc 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/components/PassthroughModelsSection.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/PassthroughModelsSection.tsx @@ -14,6 +14,7 @@ */ import React, { useState, useMemo } from "react"; import { Button } from "@/shared/components"; +import { generateUniqueModelAlias } from "./passthroughAlias.ts"; import { matchesModelCatalogQuery, normalizeModelCatalogSource, @@ -323,22 +324,18 @@ export default function PassthroughModelsSection({ : filteredModels; const activeCount = allModels.filter((model) => !model.isHidden).length; - // Generate default alias from modelId (last part after /) - const generateDefaultAlias = (modelId: string) => { - const parts = modelId.split("/"); - return parts[parts.length - 1]; - }; - const handleAdd = async () => { if (!newModel.trim() || adding) return; const modelId = newModel.trim(); - const defaultAlias = generateDefaultAlias(modelId); - // Check if alias already exists - if (modelAliases[defaultAlias]) { - alert(t("aliasExistsAlert", { alias: defaultAlias })); + // #1850: block re-adding the SAME model, but disambiguate DISTINCT models + // that would otherwise collapse to the same last-segment alias (e.g. + // enx/gpt-5.5 vs enx/codebuddy/gpt-5.5 → both "gpt-5.5"). + if (Object.values(modelAliases).includes(modelId)) { + alert(t("aliasExistsAlert", { alias: modelId })); return; } + const defaultAlias = generateUniqueModelAlias(modelId, modelAliases); setAdding(true); try { diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/ProviderModalsPanel.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/ProviderModalsPanel.tsx index b0af90baa4..8c7df58b9c 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/components/ProviderModalsPanel.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/ProviderModalsPanel.tsx @@ -31,6 +31,7 @@ import type { ProviderMessageTranslator } from "../providerPageHelpers"; interface ProviderInfo { name: string; riskNoticeVariant?: string; + website?: string; [key: string]: unknown; } @@ -277,6 +278,7 @@ export default function ProviderModalsPanel({ isOpen={showAddApiKeyModal} provider={providerId} providerName={providerInfo.name} + providerWebsite={providerInfo.website} initialBaseUrl={siliconFlowInitialBaseUrl} isCompatible={isCompatible} isAnthropic={isAnthropicProtocolCompatible} @@ -312,6 +314,7 @@ export default function ProviderModalsPanel({ isOpen={showEditModal} connection={selectedConnection} providerId={providerId} + providerWebsite={providerInfo.website} onSave={handleUpdateConnection} onResyncModels={(id) => handleCompatibleImportWithProgress(id, "sync")} onClose={() => setShowEditModal(false)} diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/WebSessionCredentialGuide.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/WebSessionCredentialGuide.tsx index f4f30ddce9..e3743acdfb 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/components/WebSessionCredentialGuide.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/WebSessionCredentialGuide.tsx @@ -10,14 +10,30 @@ import { providerText, type ProviderMessageTranslator } from "../providerPageHel export interface WebSessionCredentialGuideProps { requirement: WebSessionCredentialRequirement; providerName: string; + providerWebsite?: string; t: ProviderMessageTranslator; } +export function getProviderWebsiteHost(providerWebsite?: string): string | null { + if (!providerWebsite) { + return null; + } + + try { + return new URL(providerWebsite).host; + } catch { + return providerWebsite; + } +} + export default function WebSessionCredentialGuide({ requirement, providerName, + providerWebsite, t, }: WebSessionCredentialGuideProps) { + const providerWebsiteHost = getProviderWebsiteHost(providerWebsite); + if (requirement.kind === "none") { return (
@@ -76,6 +92,21 @@ export default function WebSessionCredentialGuide({ {providerText(t, "webSessionGuideStep1", "Sign in to {provider} in your browser.", { provider: providerName, })} + {providerWebsite && providerWebsiteHost && ( + + {providerText(t, "webSessionGuideOpenProvider", "Open {host}", { + host: providerWebsiteHost, + })} + + + )}
  • {providerText( diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx index fd112a3b0c..ca4a7cfb9c 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx @@ -2,7 +2,11 @@ import { useState, useEffect, useRef } from "react"; import { useTranslations } from "next-intl"; import { Button, Badge, Input, Modal, Toggle } from "@/shared/components"; -import { providerAllowsOptionalApiKey, supportsBulkApiKey } from "@/shared/constants/providers"; +import { + providerAllowsOptionalApiKey, + supportsBulkApiKey, + resolveWebProviderHost, +} from "@/shared/constants/providers"; import { parseBulkApiKeys } from "@/shared/utils/bulkApiKeyParser"; import { providerHasFreeModels } from "@/shared/utils/freeModels"; import { @@ -34,6 +38,7 @@ export interface AddApiKeyModalProps { isOpen: boolean; provider?: string; providerName?: string; + providerWebsite?: string; initialBaseUrl?: string; isCompatible?: boolean; isAnthropic?: boolean; @@ -56,6 +61,7 @@ export default function AddApiKeyModal({ isOpen, provider, providerName, + providerWebsite, initialBaseUrl, isCompatible, isAnthropic, @@ -85,6 +91,12 @@ export default function AddApiKeyModal({ const webSessionCredential = getWebSessionCredentialRequirement(provider); const isNoAuthWebSessionCredential = webSessionCredential?.kind === "none"; const isWebSessionCredential = !!webSessionCredential && webSessionCredential.kind !== "none"; + // #6268 — for web-session providers, resolve the provider's public site so the + // modal can offer a prominent "Open ‹host› →" link. Gated on webSessionCredential + // so non-web providers never render a link. + const webProviderHostLink = webSessionCredential + ? resolveWebProviderHost(provider, defaultBaseUrl) + : null; const providerDisplayName = providerName || provider || ""; const apiKeyOptional = providerAllowsOptionalApiKey(provider) || Boolean(isNoAuthWebSessionCredential); @@ -348,7 +360,7 @@ export default function AddApiKeyModal({ const handleBulkSubmit = async () => { if (!provider) return; - const parsed = parseBulkApiKeys(bulkText); + const parsed = parseBulkApiKeys(bulkText, { withAccountId: isCloudflare }); setBulkWarnings(parsed.warnings); if (parsed.entries.length === 0) return; @@ -378,7 +390,11 @@ export default function AddApiKeyModal({ headers: { "Content-Type": "application/json" }, body: JSON.stringify({ provider, - entries: parsed.entries.map((e) => ({ name: e.name, apiKey: e.apiKey })), + entries: parsed.entries.map((e) => ({ + name: e.name, + apiKey: e.apiKey, + ...(e.accountId ? { accountId: e.accountId } : {}), + })), priority: formData.priority || 1, providerSpecificData, validateKeys: bulkValidateKeys, @@ -421,6 +437,21 @@ export default function AddApiKeyModal({ onClose={onClose} >
    + {webProviderHostLink && ( + + + {providerText(t, "openWebProviderSite", "Open {host}", { + host: webProviderHostLink.host, + })} + + )} {bulkSupported && (