mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
* chore(release): open v3.8.31 development cycle * fix(mitm): exact host membership in MITM hosts test (CodeQL false positive) (#4386) getMitmToolHosts returns string[], so .includes(host) is Array.prototype.includes (exact membership). CodeQL's js/incomplete-url-substring-sanitization heuristic misreads it as a String.includes() URL-substring sanitization check and raises a HIGH alert. Switch to .some(h => h === host) — identical semantics, explicit intent, no flagged pattern. Surfaced post-v3.8.30 (#4325) once the test landed on main. Test-only change (no runtime behavior); the suite still passes and the CodeQL re-scan on merge clears the alert. * fix(codex): request reasoning summaries (#4359) Adds reasoning.summary=auto + reasoning.encrypted_content include for Codex. Thanks @xz-dev. * fix(embeddings): inject NVIDIA NIM input_type for asymmetric embed models (#4341) NVIDIA NIM asymmetric embedding models (e.g. nvidia/nv-embedqa-e5-v5) reject requests without an `input_type` ("query" | "passage") with 400 "'input_type' parameter is required". The embedding registry now carries a model-level default param for the asymmetric NVIDIA model, and the embeddings handler injects a model's default params into the upstream body only when the client omitted them, leaving a client-supplied value untouched. Reported-by: hydraromania (https://github.com/decolua/9router/issues/1378) Co-authored-by: hydraromania <252583922+hydraromania@users.noreply.github.com> * fix(api): migrate deprecated Codex [features].codex_hooks to [features].hooks (#4342) Codex renamed the `codex_hooks` feature flag to `hooks`; recent Codex CLI versions ignore the old key and warn. When OmniRoute rewrites an existing config.toml (configure/reset Codex provider) it now renames [features].codex_hooks -> [features].hooks, preserving the value and never clobbering an already-present `hooks`, then drops the deprecated key. The migration is a no-op when the flag is absent and runs on both the POST and DELETE config paths. Reported-by: Bian-Sh (https://github.com/decolua/9router/issues/1327) Co-authored-by: Bian-Sh <24520547+Bian-Sh@users.noreply.github.com> * fix(translator): drop the null flush on the same-format response path (#4344) The streaming response translator's same-format fast path returned `[chunk]` unconditionally, so the end-of-stream null/flush signal (chunk === null) propagated as a literal `[null]`. Downstream this surfaced as an empty `data: null` SSE event between chunks and crashed strict clients (e.g. Factory Droid BYOK on /v1/responses). The fast path now returns `[]` for the null flush while still passing real chunks through unchanged. Reported-by: thaitryhand (https://github.com/decolua/9router/issues/1052) Co-authored-by: thaitryhand <248103256+thaitryhand@users.noreply.github.com> * fix(translator): strip assistant echo fields on the OpenAI target path (Mistral 422) (#4350) Strict OpenAI-compatible upstreams (e.g. mistral/codestral-latest) reject client-only assistant echo fields sent back as input with 422 extra_forbidden (the report hit messages[].assistant.reasoning_content via Codex /responses). Only reasoning_content was stripped on the OpenAI target path; the sibling fields reasoning / refusal / annotations / cache_control leaked through. They are now all dropped on the non-reasoner OpenAI target path. `audio` is intentionally preserved (OpenAI audio models reference a prior assistant audio response by id; Mistral never emits audio). Reported-by: xxy9468615 (https://github.com/decolua/9router/issues/1649) Co-authored-by: xxy9468615 <63351664+xxy9468615@users.noreply.github.com> * fix(cli): honor TAILSCALE_AUTHKEY for non-interactive tailscale login (#4343) * fix(cli): honor TAILSCALE_AUTHKEY for non-interactive tailscale login (port from 9router#1263) startTailscaleLogin built `tailscale up` without ever reading process.env.TAILSCALE_AUTHKEY, so a pre-authenticated / headless daemon waited for an interactive auth URL and timed out (~15s). When TAILSCALE_AUTHKEY is set it is now passed via `--auth-key=` (an argv element to spawn(binary, args) — no shell interpolation, Hard Rule #13); when unset, behavior is unchanged. The arg builder is extracted into a pure exported `tailscaleUpArgs()` for testing. Reported-by: ipeterpetrus (https://github.com/decolua/9router/issues/1263) Co-authored-by: ipeterpetrus <93033698+ipeterpetrus@users.noreply.github.com> * chore(quality): rebaseline tailscaleTunnel.ts file-size to 1202 (#1263 +13) --------- Co-authored-by: ipeterpetrus <93033698+ipeterpetrus@users.noreply.github.com> * fix(dashboard): OAuth modal surfaces the real error on a non-JSON response (#4351) * fix(dashboard): OAuth modal surfaces real error on non-JSON responses (port from 9router#1318) The OAuth connect/reauth modal called `await res.json()` unconditionally, so a non-JSON error response (e.g. a plain-text 500 page from a build/OAuth endpoint) threw `Unexpected token 'I'...` and hid the real failure. New shared helpers parseResponseBody / getErrorMessage (src/shared/utils/api.ts) read the body safely (JSON when JSON, raw text otherwise) and produce a clean message either way; every modal fetch site now uses them. Reported-by: DNNYF (https://github.com/decolua/9router/issues/1318) Co-authored-by: DNNYF <74033321+DNNYF@users.noreply.github.com> * fix(dashboard): type OAuth modal response body as Record<string, unknown> (t11 any-budget) Switch the parseResponseBody casts from Record<string, any> to Record<string, unknown> so OAuthModal.tsx stays within its t11 explicit-any budget. getErrorMessage already takes unknown; the success paths typecheck clean under strict:false. No runtime change. --------- Co-authored-by: DNNYF <74033321+DNNYF@users.noreply.github.com> * fix(translator): accept AI SDK-style { type: image, image: data-URL } content parts (#4345) * fix(translator): accept AI SDK-style { type: image, image: "data:..." } parts (port from 9router#1330) Several OpenAI-input translators only recognized images shaped as `image_url.url` (or an object with `.source`/`.url`), so an AI SDK-style content part where `image` is a bare data-URL STRING was silently dropped before reaching a vision provider (OpenCode is one affected client; the gap is generic). The OpenAI->Claude, OpenAI->Kiro and OpenAI->Gemini/Antigravity translators now parse a string `image` data URL into each provider's native image shape (Claude base64 source, Kiro images[].source.bytes, Gemini inlineData). Reported-by: mugnimaestra (https://github.com/decolua/9router/issues/1330) Co-authored-by: mugnimaestra <13349159+mugnimaestra@users.noreply.github.com> * chore(quality): freeze openai-to-kiro.ts file-size at 807 (#1330 +9, over 800 cap) --------- Co-authored-by: mugnimaestra <13349159+mugnimaestra@users.noreply.github.com> * fix(dashboard): show a disabled connection's last error in the row (#4352) * fix(dashboard): show a disabled connection's last error in the row (port from 9router#1447) The provider card's error badge counts a disabled connection (isActive === false) that has an error — its effective status is still error/expired/unavailable — but the connection row hid the lastError text for disabled rows, so the operator saw the count without the cause. The row's error-visibility decision is extracted into shouldShowConnectionLastError() and now shows the error whenever there is one, regardless of the active toggle. Reported-by: ntdung6868 (https://github.com/decolua/9router/issues/1447) Co-authored-by: ntdung6868 <103993527+ntdung6868@users.noreply.github.com> * chore(quality): rebaseline ConnectionRow.tsx file-size to 942 (#1447 +1 import) --------- Co-authored-by: ntdung6868 <103993527+ntdung6868@users.noreply.github.com> * fix(providers): bound the OAuth connection-test probe with a timeout (#4347) * fix(providers): bound OAuth connection-test probe with a timeout (port from 9router#1449) The OAuth path of "Test Connection One-by-One" called bare fetch() with no AbortController/signal, so a provider probe that accepted the socket but never responded wedged the test queue forever. Both the initial probe and the post-refresh retry are now bounded with AbortSignal.timeout(30s) — matching the API-key path's existing budget — and a timed-out probe resolves as a failure with a clear "Test timed out after 30s" message in the route's normal error shape. Reported-by: ntdung6868 (https://github.com/decolua/9router/issues/1449) Co-authored-by: ntdung6868 <103993527+ntdung6868@users.noreply.github.com> * chore(quality): rebaseline providers test route file-size to 887 (#1449 + sibling #1444 growth) --------- Co-authored-by: ntdung6868 <103993527+ntdung6868@users.noreply.github.com> * fix(providers): label a deactivated account distinctly from a revoked token (#4353) A Codex connection whose OAuth refresh is fully healthy but whose ChatGPT account has been deactivated by the provider gets a 401 from the upstream API. The connection test labeled that the same as a bad credential ("Token invalid or revoked" -> upstream_auth_error), so an operator could not tell a deactivated account from a revoked token. The test now reads the 401/403 body and, when it indicates account deactivation, classifies it as account_deactivated (which the dashboard already renders as "Account Deactivated"); a plain auth 401 is unchanged. Reported-by: ntdung6868 (https://github.com/decolua/9router/issues/1444) Co-authored-by: ntdung6868 <103993527+ntdung6868@users.noreply.github.com> * fix(db): cascade-delete orphaned model aliases when a provider is removed (#4348) * fix(db): cascade-delete orphaned model aliases when a provider is removed (port from 9router#1409) Deleting a custom provider removed its connections and node but left the imported model-alias rows (key=<alias>, value="<providerId>/<model>") behind, so re-importing the same provider was blocked by stale "already exists" aliases. Add a deleteModelAliasesForProvider(providerId) DB helper that drops every alias whose stored value begins with "<providerId>/", and call it from the provider-node DELETE handler so a fresh import is unblocked. Reported-by: nguyenvanhuy0612 (https://github.com/decolua/9router/issues/1409) Co-authored-by: nguyenvanhuy0612 <57367674+nguyenvanhuy0612@users.noreply.github.com> * chore(quality): rebaseline models.ts file-size to 1221 (#1409 + sibling #1294 growth) --------- Co-authored-by: nguyenvanhuy0612 <57367674+nguyenvanhuy0612@users.noreply.github.com> * fix(api): persist max_input_tokens/max_output_tokens when adding a custom model (#4349) The POST /api/provider-models handler read the rest of the body but never the two token-limit fields, and addCustomModel() had no parameter for them, so the form values were dropped on write while the DB layer and /v1/models catalog already round-trip inputTokenLimit/outputTokenLimit. Accept the two optional limits in the schema, forward them through the handler, and persist them in addCustomModel(). TDD: failing-then-passing unit test. Reported-by: codename-zen (https://github.com/decolua/9router/issues/1294) Co-authored-by: codename-zen <263238141+codename-zen@users.noreply.github.com> * docs: feature-documentation catch-up (v3.8.20 → v3.8.30) (#4391) One-time reconciliation of the docs with every user-facing feature shipped since v3.8.20 (we had never done a dedicated pass, so debt had accumulated): - README: new '✨ What's New' section (curated v3.8.20→v3.8.30 highlights). - New guides: CLI-INTEGRATIONS (all setup-*/launch commands), MITM-TPROXY-DECRYPT (transparent-decrypt epic), CONTEXT_EDITING (delegated Anthropic clear_tool_uses). - Refreshed: AUTO-COMBO (auto/<category>:<tier> + Arena-ELO), API_REFERENCE (x-omniroute-no-memory), MEMORY (int8 quantization + off-by-default), RESILIENCE (model-lockout success-decay), RTK, AGENTBRIDGE, TRAFFIC_INSPECTOR, GUARDRAILS, CLOUD_AGENT, ENVIRONMENT, SETUP_GUIDE, CLI-TOOLS, MCP-SERVER. - Regenerated PROVIDER_REFERENCE (231 providers); synced the count in README/CLAUDE/AGENTS. - Allowlisted external-tool env vars (OPENAI_API_BASE, PROMPTFOO_PROVIDER_KEY) and the STREAM_RECOVERY config-object name in the docs-accuracy gates. All claims source-verified; check:docs-all (sync/counts/env/links/fabricated) passes. Going forward this runs every release via generate-release step 6b. * fix(executors): don't inject thinking when tool_choice forces a tool (native Claude) (#4389) Forced tool_choice now strips the adaptive thinking injection to avoid Anthropic 400. Thanks @NomenAK. * fix(translator): Gemini accepts HTTP/HTTPS image URLs (port from 9router#344) (#4373) OpenAI-style `image_url` parts with an `http://` or `https://` URL reached `convertOpenAIContentToParts` and were dropped with only a `console.warn`, because Gemini's `inlineData` requires base64 (the helper is synchronous and cannot fetch+encode upstream assets). Gemini's `Part` schema, however, natively accepts `fileData: { fileUri }` for remote URIs — the model fetches the asset itself. The helper now emits a `fileData` part (`mimeType: "image/*"`, inferred upstream on fetch) for HTTP/HTTPS URLs instead of silently dropping them. Vision requests that pass a URL — not a data: URI — now reach Gemini intact. No behavioral change for: - `data:` URIs → still emitted as `inlineData` with the parsed media type. - Unsupported schemes (e.g. `ftp:`) → still skipped (Gemini would reject them). The openai-to-claude side already passed HTTP/HTTPS URLs through as `source: { type: "url", url }` (lines 573–578) — the upstream PR's Claude-side change was already covered. Regression test: tests/unit/gemini-helper-http-image-url-port344.test.ts (4 cases: https URL, http URL, data: URI no-regression, unsupported-scheme guard). Inspired-by: https://github.com/decolua/9router/pull/344 Co-authored-by: Ibrahim Ryan <ryan@nuevanext.com> * fix(executors): strip stream_options for qwen non-streaming / thinking Claude Code requests (port from 9router#663) (#4374) Claude-Code-compatible providers force the executor-level `stream` flag on via `upstreamStream = stream || isClaudeCodeCompatible` (open-sse/handlers/chatCore.ts), but the outgoing body keeps the caller's original `stream: false`. The shared `stream && targetFormat === "openai"` branch in DefaultExecutor.transformRequest then injected `stream_options: { include_usage: true }` onto a body that still said `stream: false`, and qwen upstream rejected the request with `400 "'stream_options' only set this when you set stream: true"`. The same rejection surfaced when the body carried `thinking` / `enable_thinking`. The qwen branch now skips the injection (and strips any client-sent `stream_options`) when the body explicitly says `stream: false` or requests thinking, leaving regular qwen streaming requests with the include_usage injection intact. Other providers are unaffected. Adds a TDD regression with 4 cases covering both opt-out paths and the normal-streaming positive control. Inspired-by: https://github.com/decolua/9router/pull/663 Co-authored-by: anuragg-saxenaa <anuragg.saxenaa@gmail.com> * fix(security): scope OAuth callback postMessage to a trusted-origin allowlist (port from 9router#998) (#4372) The OAuth callback at `/callback` previously fell back to `window.opener.postMessage({ code, state, ... }, "*")` whenever the opener was cross-origin. The fallback was intended to support remote-OmniRoute + local-loopback callbacks (where opener and callback live on different origins), but the same code path also delivers the OAuth code/state to any hostile opener that pops the well-known callback URL — letting that attacker complete the OAuth flow as the user. Replace the wildcard fallback with iteration over a fixed allowlist: `window.location.origin` (same-origin parent — the popup-mode dashboard) plus Codex's fixed loopback helper (`http://localhost:1455` and the IPv4 literal `http://127.0.0.1:1455`). The browser drops `postMessage` to any opener whose actual origin is not in `targetOrigin`, so the message reaches only known parents and is silently dropped for any other. The same-origin fallback path is unchanged — methods 2 (`BroadcastChannel`) and 3 (`localStorage` storage event) still cover same-origin openers that COOP severed. The `openerSameOrigin` probe stays in place to drive the auto-close vs manual-copy UI decision (no behavior change for the success path). Adds a regression test (`tests/unit/ui/oauth-callback-postmessage-scope.test.tsx`) that mounts the page with a stubbed cross-origin opener and asserts no `postMessage` call ever uses `"*"` and every call lands on an allowlisted origin. The test failed against the pre-fix code (red), passes after the fix (green) — TDD per CLAUDE.md hard rule #18. Partial port of upstream decolua/9router#998: the upstream PR also re-enabled TLS verification on a DNS-bypass fetch in `open-sse/utils/proxyFetch.js`; that part is N/A here because OmniRoute's `proxyFetch.ts` never disabled TLS verification (no `rejectUnauthorized: false` anywhere in the file). Inspired-by: https://github.com/decolua/9router/pull/998 Co-authored-by: aeonframework <aeon@aeonframework.dev> * fix(sse): default combo per-target timeout to 120s for fast failover (#4365) Combo per-target timeout inherited the full FETCH_TIMEOUT_MS (600s) when a combo did not set its own targetTimeoutMs, so a single hung/slow target stalled the whole combo for up to 10 minutes before falling through to the next model. Introduce DEFAULT_COMBO_TARGET_TIMEOUT_MS (120s) as the unset-default in resolveComboTargetTimeoutMs (new 3rd arg) and wire it in phaseComboSetup. The upstream ceiling (600s) and per-combo opt-out (targetTimeoutMs, up to the ceiling) are preserved; single non-combo requests are unchanged. For streaming requests this only bounds time-to-first-headers, so token generation is not cut short. TDD: failing-then-passing unit test in tests/unit/combo-config.test.ts. * refactor(combo): de-dup exhausted-target skip predicate across both dispatchers (#4362) Primeiro incremento da de-dup dos 2 dispatchers de combo (handleComboChat + handleRoundRobinCombo). O bloco de pre-check #1731/#1731v2 (skip de target já exhausted no provider/connection) era BYTE-IDÊNTICO nos dois (mesmas condições, mesmas mensagens), diferindo só na tag de log e no control-flow. - comboPredicates.ts: getExhaustedTargetSkipReason(target, exhaustedProviders, exhaustedConnections) — predicate PURO que retorna a mensagem de skip (ou null); cada dispatcher mantém seu próprio log-tag + control-flow (return null / continue) + fallbackCount. No mutate do stryker (cobertura de mutação). - combo.ts: −20 linhas (os 2 blocos viram 1 chamada cada). - 7 testes de caracterização travam condições + strings exatas. Comportamento preservado: 376/376 testes combo (357 caracterização + 7 novos), integração sse-correctness 5/5, typecheck 0, complexity neutro (1895), file-size encolhe. Próximo incremento: de-dup do error-handling/exhausted-tracking (handleTargetError). * refactor(combo): de-dup upstream-error exhaustion classification across both dispatchers (#4366) Segundo incremento da de-dup dos 2 dispatchers (handleTargetError). Após cada erro de target, ambos rodavam um bloco quase-idêntico que marca o provider exhausted (#1731), a conexão connection-errored (#1731v2) ou o provider transiently rate-limited. - combo/targetExhaustion.ts: applyComboTargetExhaustion(target, opts) — atualiza os 3 Sets de exhaustion e retorna providerExhausted. As MUTAÇÕES de Set (que dirigem o skip de targets, lidas por getExhaustedTargetSkipReason) são BYTE-IDÊNTICAS nos dois; as diferenças reais viram parâmetros: tag, allAccountsRateLimited (termo extra do RR, false no handleComboChat), exhaustedLogLevel (info no handleComboChat, debug no RR). Connection-level extraído p/ markConnectionLevelExhaustion (privado, <15 complexity). - combo.ts: −73 linhas; 4 imports órfãos removidos. - 7 testes de caracterização travam as mutações + o return. ÚNICA mudança de comportamento: o WORDING das mensagens de log do RR ganha o sufixo 'on remaining targets' (cosmético; mesmo #code, mesmas mutações, mesmos níveis de log). 376/376 combo (caracterização preservada), integração sse 5/5, typecheck 0, complexity neutro (1895), file-size encolhe. * refactor(chatCore): extract checkHeapPressureGuard leaf (god-file decomposition start) (#4371) Primeiro incremento da decomposição do chatCore.ts (5127 LOC, hot-path mais quente). O guard de memória do topo do handleChatCore (rejeita 503 quando o heap V8 passa o threshold de shed) vira um leaf testável, co-locado com o threshold em heapPressure.ts. - heapPressure.ts: checkHeapPressureGuard(heapUsedMb, thresholdMb) — retorna o result 503 pronto ou null. Byte-idêntico ao guard inline (mesmo check, mesma 503, mesmo warn). A figura de heap fica em telemetria INTERNA, nunca no response do cliente (Hard Rule #12). - chatCore.ts: o bloco inline (~22 ln) vira 3 linhas; import órfão de HEAP_PRESSURE_THRESHOLD_MB trocado por checkHeapPressureGuard. - 3 testes novos (incl. assert Rule #12: o MB medido não vaza no payload). complexity-baseline 1895->1896: drift de base pós-#4338 (medido com minhas mudanças stashed = 1896); esta mudança é complexity-NEUTRA (helper complexity 2, handleChatCore só perde código). 190/190 chatcore tests, typecheck 0, file-size encolhe. * Localize CLI and stabilize fetch, memory, and coverage handling (#4383) en-only i18n, fetch-start-timeout hardening, EngineConfigPage icon fix, CI build-artifact-reuse overhaul. Memory production hunk dropped as a no-op (tests kept). Thanks @JxnLexn. * test(combo): reset circuit breakers between stream-readiness cases (restore green) (#4396) The combo-dispatch cases in combo-stream-readiness-fallback.test.ts deliberately fail `glm` (zombie streams / repeated 504s), which legitimately trips the per-provider circuit breaker. That OPEN state is a module-level singleton, so it leaked into the next test and combo.ts then SKIPPED `glm/*` targets entirely ("Skipping … circuit breaker OPEN"). That made "combo does not retry stream readiness timeouts on the same model" never attempt glm/zombie — expected ['glm/zombie','openai/gpt-5.4-mini'] but got ['openai/gpt-5.4-mini']. This was a pre-existing red on release/v3.8.31 (present at the cycle-open tip), order-dependent: the test passes in isolation, fails after the preceding cases. Add a test.beforeEach(resetAllCircuitBreakers) so each scenario starts from a clean breaker slate. Test-isolation only — the breaker behavior is correct and no production code or assertion changes. Full combo suite: 390/390 green. * fix(cli): decline confirm() cleanly on non-interactive stdin + document contexts workflow The `contexts remove` command already has `--yes` to skip confirmation, but when run without it under a non-interactive stdin (pipe, CI, EOF) the [y/N] prompt could never be answered — the readline question stayed pending and Node warned about an "unsettled top-level await" at exit. confirm() now detects `!process.stdin.isTTY` and declines cleanly (returns false), pointing at `--yes` for non-interactive use. Exported confirm() for a regression test. Docs: REMOTE-MODE.md gains a full "Managing contexts" section (list/current/use to switch between remote and local, add/show/rename, remove with --yes, export/import), fixes a `context current` -> `contexts current` typo, and the README remote-mode snippet now shows switching back to local. Verified against the live CLI: command signatures, --yes, and the non-TTY decline path all behave as documented. Tests: cli-contexts.test.ts asserts confirm() declines on non-TTY stdin (RED before, GREEN after). All docs gates (fabricated/links/symbols) pass. * ci(t11): bump any-budget for executors/base.ts (2 false-positive "any" strings) Unblocks the Fast Quality Gates on release/v3.8.31: `check:any-budget:t11` was red on `open-sse/executors/base.ts` for ALL PRs (pre-existing base drift, unrelated to this branch). The checker counts `\bany\b` after stripping comments but NOT strings, and the native-Claude tool_choice logic uses the API value `"any"` in two string literals (`tb.tool_choice === "any"`, `.type === "any"`). There are zero actual TypeScript `any` types in the file — budget set to the matched count, mirroring the existing cursor.ts false-positive entry right below it. * perf: combos UI split + next config + 1-click redis + bifrost sidecar (#3932) (#4381) Combos UI split + next.config perf + 1-click local Redis launcher + bifrost relay. Review fixes (co-author): --rm/--restart conflict, error sanitization, UI/route names, dead-guard re-doc, bifrost Zod, IPv6 + CLI test fixes. Thanks @KooshaPari. * fix(plugin): prefix OC static-catalog combo+raw keys with providerId (#4384) OC parses model ids on '/'; combo keys now carry 'omniroute/' (was 'combo/'). Live-validated against the VPS via OpenCode. Thanks @herjarsa. * docs(env): document TAILSCALE_AUTHKEY (env/docs contract drift on .31) Second pre-existing base-drift fix needed to get Fast Quality Gates green: the `repository contract is in sync` test (check:env-doc-sync) was red on ALL .31 PRs. PR #4343 added a code reference to `process.env.TAILSCALE_AUTHKEY` (src/lib/tailscaleTunnel.ts) for non-interactive `tailscale up`, but never added the var to .env.example / docs/reference/ENVIRONMENT.md — the contract requires code vars to appear in both. Add the (commented) entry to .env.example next to TAILSCALE_BIN and a row to ENVIRONMENT.md. Verified: `node scripts/check/check-env-doc-sync.mjs` → in sync. Unrelated to this branch's confirm()/docs change; surfaced because touching a quality script triggers the TIA fail-safe full unit suite. * chore(release): v3.8.31 — 2026-06-20 Finalize the v3.8.31 release: reconcile the CHANGELOG (full commit-to-bullet coverage, 26 bullets across Features/Fixed/Security/Maintenance), refresh the README What's New section, back-fill the .30/.31 sections into the 41 i18n CHANGELOG mirrors, and add TAILSCALE_AUTHKEY to the env contract (.env.example + ENVIRONMENT.md). * chore(release): align mitm-hosts test comment with main to clear merge conflict The same CodeQL false-positive fix landed twice — #4386 on release/v3.8.31 and #4387 directly on main — with only the explanatory comment differing (the assertion is byte-identical). Adopt main's comment wording on the release branch so the release PR merges without a comment-only conflict. * test(translator): align stale openai->gemini remote-URL tests with #4373 Third pre-existing base-drift fix to get Fast Quality Gates green on .31. PR #4373 ('Gemini accepts HTTP/HTTPS image URLs', port of 9router#344) intentionally changed convertOpenAIContentToParts so remote http(s) image URLs pass through as a native `fileData: { fileUri }` part instead of the old #2807 drop+warn — and added its own test (gemini-helper-http-image-url-port344.test.ts) for the new behavior. But it left three tests in translator-openai-to-gemini.test.ts asserting the OLD drop+warn contract, so they fail deterministically (verified: the file fails in isolation on clean .31). These only surface under the TIA fail-safe FULL suite, which a quality- script touch triggers. Align the three stale tests to the real, intended behavior (captured by running the function): remote URLs -> fileData.fileUri (mimeType image/*), still never inlineData (the sync path cannot fetch+encode). This is test-vs-code alignment to a deliberate, separately-tested change — not a weakened assertion. 40/40 in the file; 94/94 across the translator + env-doc combo that previously failed. * chore(quality): reconcile complexity baseline 1896->1900 (/review-prs v3.8.31 batch) (#4410) * fix(release): reconcile full-CI drift for v3.8.31 (gemini tests #4373, any-budget #4389, masking allowlist #4384) The release PR's full CI surfaced cumulative cycle drift the per-PR fast gates skip: - tests/unit/translator-openai-to-gemini.test.ts: realign 3 cases to #4373's HTTP/HTTPS-URL fileData pass-through (they asserted the old warn-and-drop). - scripts/check/check-t11-any-budget.mjs: base.ts budget 0→2 — #4389 compares tool_choice against the string literal "any" (not a TS any type). - config/quality/test-masking-allowlist.json: allowlist #4384's opencode combos net-assert reduction (obsolete combo/ namespace removed). No production behavior change. * chore(release): re-trigger full CI for v3.8.31 finalization Force a fresh pull_request CI run on the head carrying the cycle-drift fixes (gemini #4373 tests, any-budget #4389, masking allowlist #4384) — the prior synchronize event did not spawn a ci.yml run. * chore: re-trigger CI (no Actions runs registered for prior push) --------- Co-authored-by: Xiangzhe <32761048+xz-dev@users.noreply.github.com> Co-authored-by: hydraromania <252583922+hydraromania@users.noreply.github.com> Co-authored-by: Bian-Sh <24520547+Bian-Sh@users.noreply.github.com> Co-authored-by: thaitryhand <248103256+thaitryhand@users.noreply.github.com> Co-authored-by: xxy9468615 <63351664+xxy9468615@users.noreply.github.com> Co-authored-by: ipeterpetrus <93033698+ipeterpetrus@users.noreply.github.com> Co-authored-by: DNNYF <74033321+DNNYF@users.noreply.github.com> Co-authored-by: mugnimaestra <13349159+mugnimaestra@users.noreply.github.com> Co-authored-by: ntdung6868 <103993527+ntdung6868@users.noreply.github.com> Co-authored-by: nguyenvanhuy0612 <57367674+nguyenvanhuy0612@users.noreply.github.com> Co-authored-by: codename-zen <263238141+codename-zen@users.noreply.github.com> Co-authored-by: Anton <39598727+NomenAK@users.noreply.github.com> Co-authored-by: Ibrahim Ryan <ryan@nuevanext.com> Co-authored-by: anuragg-saxenaa <anuragg.saxenaa@gmail.com> Co-authored-by: aeonframework <aeon@aeonframework.dev> Co-authored-by: Jan Leon <Jan.gaschler@gmail.com> Co-authored-by: KooshaPari <42529354+KooshaPari@users.noreply.github.com> Co-authored-by: Hernan Javier Ardila Sanchez <hjasgr@gmail.com>
🌐 Multilingual Documentation — OmniRoute
Translations of documentation into 40 languages. Code blocks remain in English.
- 🇸🇦 العربية (
ar): Docs Root - 🇦🇿 Azərbaycan dili (
az): Docs Root - 🇧🇬 Български (
bg): Docs Root - 🇧🇩 বাংলা (
bn): Docs Root - 🇨🇿 Čeština (
cs): Docs Root - 🇩🇰 Dansk (
da): Docs Root - 🇩🇪 Deutsch (
de): Docs Root - 🇪🇸 Español (
es): Docs Root - 🇮🇷 فارسی (
fa): Docs Root - 🇫🇮 Suomi (
fi): Docs Root - 🇫🇷 Français (
fr): Docs Root - 🇮🇳 ગુજરાતી (
gu): Docs Root - 🇮🇱 עברית (
he): Docs Root - 🇮🇳 हिन्दी (
hi): Docs Root - 🇭🇺 Magyar (
hu): Docs Root - 🇮🇩 Bahasa Indonesia (
id): Docs Root - 🇮🇹 Italiano (
it): Docs Root - 🇯🇵 日本語 (
ja): Docs Root - 🇰🇷 한국어 (
ko): Docs Root - 🇮🇳 मराठी (
mr): Docs Root - 🇲🇾 Bahasa Melayu (
ms): Docs Root - 🇳🇱 Nederlands (
nl): Docs Root - 🇳🇴 Norsk (
no): Docs Root - 🇵🇭 Filipino (
phi): Docs Root - 🇵🇱 Polski (
pl): Docs Root - 🇵🇹 Português (Portugal) (
pt): Docs Root - 🇧🇷 Português (Brasil) (
pt-BR): Docs Root - 🇷🇴 Română (
ro): Docs Root - 🇷🇺 Русский (
ru): Docs Root - 🇸🇰 Slovenčina (
sk): Docs Root - 🇸🇪 Svenska (
sv): Docs Root - 🇰🇪 Kiswahili (
sw): Docs Root - 🇮🇳 தமிழ் (
ta): Docs Root - 🇮🇳 తెలుగు (
te): Docs Root - 🇹🇭 ไทย (
th): Docs Root - 🇹🇷 Türkçe (
tr): Docs Root - 🇺🇦 Українська (
uk-UA): Docs Root - 🇵🇰 اردو (
ur): Docs Root - 🇻🇳 Tiếng Việt (
vi): Docs Root - 🇨🇳 中文 (简体) (
zh-CN): Docs Root