Files
OmniRoute/docs/i18n
Diego Rodrigues de Sa e Souza 3cd1899484 fix(cli): non-interactive confirm() + document the contexts workflow (#4397)
* 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>
2026-06-20 15:53:35 -03:00
..

🌐 Multilingual Documentation — OmniRoute

Translations of documentation into 40 languages. Code blocks remain in English.