Compare commits

..

131 Commits

Author SHA1 Message Date
nguyenha935
d4b9ce6016 fix(kiro): harden auth flows, quota lookup, and model discovery (#8565)
* fix(kiro): fetch builder id quota without profile arn

* fix(kiro): harden auth imports polling and model discovery

* fix(kiro): preserve auth identity and OAuth polling semantics

* docs(changelog): add Kiro auth and model discovery fix

---------

Co-authored-by: Nguyễn Thanh Hà <nguyenha@Mac-mini-M4.local>
Co-authored-by: nguyenha935 <208228297+nguyenha935@users.noreply.github.com>
2026-07-26 03:53:47 -03:00
Adrian Rogala
2614e8a223 feat(cli-tools): add all Hermes Agent auxiliary model roles (#8543)
* feat(cli-tools): add all Hermes Agent auxiliary model roles

Extend HERMES_AGENT_ROLES from 7 to 18 slots to match the full
auxiliary.* set in Hermes Agent config.yaml:

- add: mcp, title_generation, memory_query_rewrite, tts_audio_tags,
  triage_specifier, kanban_decomposer, profile_describer, goal_judge,
  curator, monitor, background_review
- reorder: web_extract before compression (match upstream docs)

Backend generator/reader are generic on auxiliary.<role> — only the
role catalog, UI card, and i18n (en + pl) needed updating.

* fix(i18n): translate the 12 new Hermes auxiliary roles into vi and pt-BR

The 22 new en.json keys landed only in pl.json, breaking the vi and pt-BR
key-parity guards. Adds the same keys with real translations (no placeholder
strings), keeping both parity assertions exact.

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>

* test(cli-helper): guard HERMES role catalog parity between backend and UI

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-26 03:53:36 -03:00
Hernan Javier Ardila Sanchez
3be0a5d290 fix: combo input-bound, Responses->Chat image strip, qwen-web toolCalling, empty-response exhaustion (#8476)
* test(tail): retire stale i18n __MISSING__ repro + fix qianfan website URL

Base-red slice 6, rebased onto the advanced release/v3.8.49 (91fd5f9). The oauth
grok-cli #7610 guard was already fixed on the base by #8027 (it reads the warning
from grokCliAuthJson.ts) — dropped from this slice to avoid a conflicting duplicate.
Remaining two, still red on the current base:

- i18n #7258: the "focused repro" asserted zh-TW.json STILL carries raw __MISSING__:
  placeholders. That backlog was filled (the "no locale has a raw __MISSING__: leaf"
  invariant is the durable guard); retired the now-inverted repro.
- qianfan: Baidu renamed the product page (product/wenxinworkshop -> product-s/
  qianfan_home); updated the expected website URL.

Validated (clean env): i18n 4/0, qianfan 5/0; oauth-modal-grok 2/0 already green on base.

* fix(resilience): short-circuit combo on input-bound failures (context_length_exceeded) (#8375)

isInputBoundRequestFailure() predicate detects deterministic input-bound
errors (context_length_exceeded/context_window_exceeded). The combo loop
propagates the original 400 immediately instead of burning MAX_GLOBAL_ATTEMPTS
retrying identical oversized inputs against every account.

Test: combo-input-bound-failure-8375.test.ts (1 test, 2 assertions)

* fix(resilience): add early-exit in combo dispatcher for input-bound failures (#8375)

When isInputBoundRequestFailure detects context_length_exceeded,
the combo loop returns {ok:false, response} immediately instead of
re-dispatching the oversized request.

Test: node --import tsx/esm --test tests/unit/combo-input-bound-failure-8375.test.ts
- 1 test, 2 assertions, 0 fail

* fix(translator): strip input_image from tool outputs in Responses->Chat downgrade (#8459)

toolOutputContentToString() extracts input_text/output_text parts and
replaces input_image with a placeholder instead of JSON.stringify'ing
the content-part array (which embedded raw ~52KB base64 as inert text).

Applied to both function_call_output and custom_tool_call_output branches.

Existing translator tests: 88/88 pass.
New tests: 4/4 pass.

* fix(providers): set qwen-web toolCalling to false — web-cookie provider has no native function calling (#8437)

qwen-web is a web-cookie provider that emulates tools via synthetic system
prompt text and <tool> XML parsing, never sending a native tools[] field
upstream. The filterTargetsByRequestCompatibility gate filters out non-tool-
calling targets when the request carries tools, but qwen-web's registry entry
had toolCalling=true, so the filter let it through and a tool-using session
failing over to qwen-web would silently degrade to text-only chat with
'Tool X does not exists' errors.

Sibling web-cookie providers (chatgpt-web, yuanbao-web, claude-web, etc.)
all correctly set toolCalling: false — qwen-web was an outlier introduced
in PR #7874.

Verification:
- LSP diagnostics: clean
- Pattern matches chatgpt-web, yuanbao-web, and other web-cookie providers

* fix(backend): empty upstream response mislabeled as exhausted_connection (#8397)

isEmptyContentFailure guard only matched '/empty content/i' but the actual
error text from detectMalformedNonStream is 'returned an empty response
(no usable choices/output)' — which lacks the word 'content'. Expanded
regex to also match '/empty response/i' so these transient upstream glitches
don't get classified as connection-level exhaustion in combo diagnostics.

Test: 28 existing combo-target-exhaustion tests pass (no new test needed)

* test(#8397): add regression test for empty-response 502 not marking provider/connection exhausted

* test(qwen-web): align registry snapshot with toolCalling:false

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(resilience): scope #8375 input-bound short-circuit to homogeneous remainders

The isInputBoundFailure short-circuit (context_length_exceeded /
context_window_exceeded) fired unconditionally on the first target, aborting
the whole combo even when later targets are a different model with a larger
context window — regressing the intentional heterogeneous-combo fallback that
isContextOverflow400 (#6637) protects. Reproduced with a 2-target combo
(small-context model fails, larger-context model would have succeeded): the
combo never reached target 2.

Scope the short-circuit to remainders where every remaining target shares the
same modelStr as the one that just failed — the "retrying will fail
identically" premise for context_length_exceeded only holds within a
homogeneous same-model pool.

Rebaselines open-sse/services/combo.ts's frozen file-size cap (3642->3679)
for this PR's own combo.ts growth (config/quality/file-size-baseline.json).

Refs #8375

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>

---------

Co-authored-by: Probe Test <probe@example.com>
Co-authored-by: herjarsa <herjarsa@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: ikelvingo <im.kelvinwong@gmail.com>
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-07-26 03:53:32 -03:00
Harvey Doan
a095ebc43d feat: read INITIAL_PASSWORD env var during setup (#8439)
* feat: read INITIAL_PASSWORD env var during setup

Allow users to set the admin password via the INITIAL_PASSWORD
environment variable instead of requiring the --password CLI flag
or interactive prompt. Falls between --password flag and interactive
prompt in resolution priority.

* test(cli): cover INITIAL_PASSWORD env var in setup resolvePassword

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: linh.doan <linh.doan@be.com.vn>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-26 03:53:28 -03:00
Dizzle
ca79344b40 fix(cli): backup create/auto enable — remove option-shadowing legacy fallback (#8512) (#8516)
Co-authored-by: Max <maxmad64@gmail.com>
2026-07-26 03:53:25 -03:00
Prudhvi Vuda
ac5691f04c fix(providers): resolve native vision for path-shaped multimodal model ids (#8495)
* fix(providers): resolve native vision for path-shaped multimodal model ids

Leaf-id static/registry metadata now wins over synced attachment=false without
modalities, so cp/cline-pass/kimi-k3 forwards images natively.

Closes #8032

* fix(providers): scope path-shaped leaf lookup to vision only

Move leaf MODEL_SPECS fallback out of shared getStaticSpec() so
aihorde/deepseek/deepseek-v4-flash no longer inherits DeepSeek tool-calling
metadata (#8212). Vision still resolves via getVisionStaticSpec().

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>

---------

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-07-26 03:53:21 -03:00
Prudhvi Vuda
5bffb57b44 fix(providers): honor Extra API Keys rotation in OpencodeExecutor (#8493)
* fix(providers): honor Extra API Keys rotation in OpencodeExecutor

OpencodeExecutor.buildHeaders bypassed resolveEffectiveKey, so extraApiKeys
never rotated and an empty primary with populated extras sent no auth header.

Closes #8467

* fix(executors): gate the Authorization header write on the resolved effective key

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-26 03:53:18 -03:00
NOXX - Commiter
a08b5cdf4e fix(notion-web): mint fresh thread for new OpenAI sessions with same opener (#8511)
Sticky root keys keyed only on the first user message caused Claude Code
"New session" + "hi" to reuse a confirmed prior Notion thread (forking history).

Prefer exact conversation-prefix match for multi-turn; keep sticky root for
UREW multi-turn and failed-first-request retries; mint createThread:true when
the sticky root is already confirmed and the request has no assistant history.
2026-07-26 03:53:14 -03:00
NOXX - Commiter
26d50f1913 feat(adobe-firefly): reference image attach + /v1/images/edits (follow-up #8006) (#8510)
* feat(adobe-firefly): reference image attach + /v1/images/edits (follow-up #8006)

Upload source images to Firefly storage (POST /v2/storage/image) and attach
them as referenceBlobs on generate-async, matching live firefly.adobe.com
captures (usage:general for nano multi-ref; usage:subject for gpt-image).

Also wire built-in adobe-firefly through OpenAI-compatible POST /v1/images/edits
(multipart or JSON data URLs, up to 4 refs) so Media edit-with-references
and Open WebUI image-edit hit the same path as image2image generate.

Unit suite: tests/unit/adobe-firefly.test.ts 41/41.

* test(api): add route-level coverage for Adobe Firefly /v1/images/edits + fix typecheck/file-size drift

Covers the referenceBlobs upload path, the 4-reference cap error, and the
credentials/rate-limit branches added to the /v1/images/edits route for
adobe-firefly (#8510). Also fixes a Buffer/BodyInit typecheck mismatch in
uploadAdobeFireflyImage and corrects the adobeFireflyClient.ts file-size
baseline entry to match the gate's actual LOC count (it counts the trailing
newline, so the frozen value is 2317, not 2316), plus a testFrozen entry for
adobe-firefly.test.ts's own +159 line growth from this PR. Moves the
handleAdobeFireflyImageGeneration re-export out of the middle of the import
block in imageGeneration.ts for readability.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(security): restore bounded JWT regex quantifiers dropped by edit-route refactor

The edits-route extraction (test commit 62785e972f) had accidentally
reverted 4 JWT-extraction regexes in adobeFireflyClient.ts from the
bounded {1,4096} quantifier back to unbounded +, undoing the ReDoS fix
from #8173 (CodeQL #754/#755/#756). Restored the bounded quantifiers;
behavior is unchanged (45/45 adobe-firefly tests still pass), only the
backtracking bound is back in place.

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>

---------

Co-authored-by: ikelvingo <im.kelvinwong@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-07-26 03:53:10 -03:00
NOXX - Commiter
2d48bb6dca fix(hyperagent): default 1M context for fable/opus/sonnet (#8496)
* fix(hyperagent): default 1M context for fable/opus/sonnet

HyperAgent Claude-family models (fable, opus, sonnet) were falling through
getTokenLimit to the generic 128k default. Agentic tool-loop prompts with
large catalogs then failed with context_length_exceeded (~137k tokens).

- defaultContextLength + per-model contextLength = 1_000_000 on hyperagent registry
- DEFAULT_LIMITS.hyperagent / ha = 1M
- Resolve hyperagent/ha (and fable/opus wire ids) before models.dev DB fallback

Verified: getTokenLimit('hyperagent','fable-latest') === 1000000; context-manager tests 30/30.

* fix(sse): scope hyperagent 1M context fix to the registry, drop unscoped model-name match

The step-1b branch in resolveTokenLimit() matched fable/opus/sonnet model
name substrings for ANY provider, before the models.dev DB lookup. That
collided with anthropic/claude, kiro, windsurf and bluesminds registries,
which serve the same Claude model ids (e.g. claude-opus-4.7-max,
claude-sonnet-5) with their own accurate per-model contextLength — those
were being clobbered to 1M instead of their real (often 200k) limit.

The registry-level defaultContextLength added on the hyperagent provider
entry already fixes the reported bug (getTokenLimit('hyperagent', ...) ===
1_000_000) on its own, scoped correctly by provider. Remove the redundant,
unscoped substring branch and add regression coverage for every hyperagent
fallback model id plus the cross-provider collision.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-26 03:53:06 -03:00
Diego Rodrigues de Sa e Souza
fae10668ba fix(api): expose responses-format models on all VS Code Ollama listing routes (#7587) (#8564)
isUsableChatModel() was copy-pasted into 5 vscode listing routes. PR #7012
widened only models/route.ts to accept api_format "responses"/"openai-responses"
alongside "chat-completions"; the other 4 copies (token+raw api/tags and
api/show) still rejected anything that wasn't literally "chat-completions",
silently dropping Codex-discovery-synced GPT models (apiFormat "responses")
from the Ollama-compatible /api/tags endpoint VS Code's "Ollama" provider
import flow actually calls.

Extracted the predicate into a single shared module
(vscode/[token]/usableChatModel.ts) imported by all 5 routes so this
one-fixed-four-left-behind drift cannot recur.

Co-authored-by: ikelvingo <im.kelvinwong@gmail.com>
2026-07-26 03:53:02 -03:00
Diego Rodrigues de Sa e Souza
a784c52d34 fix(api): warn (never reject) when a combo name shadows a real model id (#8530) (#8563)
POST /api/combos and PUT /api/combos/[id] had zero validation or
observability when a combo name collided with a real model id, and
sseModelService.getComboForModel() always resolves the combo first. That
combo-first precedence is not a bug: #6940 documents a combo named after a
bare model id (e.g. `gpt-5.5`) as the supported mechanism for per-model
provider fallback, reusing the #3227/#3233 machinery and covered by
tests/unit/responses-combo-resolution-3227.test.ts and
tests/unit/combo-name-codex-responses-rewrite.test.ts. Hard-rejecting a
colliding name (as #8530's literal acceptance criteria requested) would
regress that documented workflow.

Instead, both routes now attach a non-blocking `warning` field
(`COMBO_NAME_SHADOWS_MODEL`) to the create/rename response when the name
collides with a real model id, and a new boot-time scan
(scanComboModelNameCollisionsAtBoot in src/instrumentation-node.ts) logs a
startup warning enumerating existing collisions — so an operator who hits
this by accident has a signal, while the #6940-sanctioned pattern keeps
working exactly as before.

New tests/unit/combo-model-name-collision-8530.test.ts proves both: the
sanctioned shapes (create/rename to a colliding name) still return
201/200 with the warning attached, and non-colliding names get no warning
field at all.

Co-authored-by: ikelvingo <im.kelvinwong@gmail.com>
2026-07-26 03:52:58 -03:00
Diego Rodrigues de Sa e Souza
7b3ef477f3 fix(providers): persist runtime-discovered Antigravity projectId to the connection (#8491) (#8562)
Co-authored-by: ikelvingo <im.kelvinwong@gmail.com>
2026-07-26 03:52:54 -03:00
MumuTW
4c9292e66e chore(i18n): normalize zh-TW terminology and sync stale README figures (#8554)
The zh-TW catalog was machine-translated with mainland-habit vocabulary and
simplified->traditional conversions that picked the wrong homophone, and its
README still advertised the v3.7-era figures.

Terminology (docs/i18n/zh-TW/ + src/i18n/messages/zh-TW.json):
- wrong-character conversions: 上遊->上游, 後臺->後台, 儀錶板->儀表板
- mainland habits: 默認->預設, 緩存->快取, 模塊->模組, 調用->呼叫,
  字符串->字串, 全局->全域, 文檔->文件, 響應->回應
- consistency: 供應商/提供商->提供者 (提供者 was already 71% dominant),
  型別->類型 for UI labels, 不活躍->未啟用

README figures synced to the English source: 231->290 providers,
17->19 routing strategies, 1.6B->1.53B free tokens, 50+->90+ free tiers,
11->40+ free forever, 87->104 MCP tools, 30->31 scopes.

Root cause — the generator's post-translation pass was a hardcoded list that
duplicated the glossary and was wired only into the deprecated
generate-multilang.mjs, so the active run-translation.mjs pipeline applied
nothing. Worse, its blanket /代碼/g -> 程式碼 rule would corrupt 控制代碼
(handle), 語系代碼 (locale code) and 錯誤代碼 (error code) on the next
regeneration.

Both scripts and the drift gate now share scripts/i18n/glossary-normalize.mjs,
driven by scripts/i18n/glossary/<locale>.json as the single source of truth.
Ambiguous terms carry blockedPrefixes so 型別->類型 can stay enforced without
mangling 模型別名 (model alias) or 基本型別 (a programming data type); terms
whose synonym is also a legitimate rendering elsewhere (代碼, 項目) are seeded
with no synonyms and documented instead of blanket-rewritten.

CI now runs the glossary gate for zh-TW alongside zh-CN.
2026-07-26 03:52:50 -03:00
MumuTW
cbdf1fc835 fix(backend): bound the client raw request snapshot instead of deep-cloning the body (#7847) (#8550)
buildClientRawRequest deep-cloned the ENTIRE request body on every chat request, unbounded.
On the #7847 incident payload (3.05 MiB, 729 messages, 86 tools) that retains 3.19 MiB per
request, and it is pure waste: every consumer of clientRawRequest.body is observability and
none of them keeps the full payload.

  chatCore.ts -> reqLogger.logClientRawRequest   no-op when the logger is disabled, otherwise
                                                 re-clones via cloneBoundedForLog (0.08 MiB)
  chatCore.ts -> trackPendingRequest             clientRequest, surfaced by /api/logs/[id]
  chat.ts     -> recordRejectedRequestUsage      requestBody

None feeds dispatch, translation or the upstream request, so the snapshot is now taken with
cloneBoundedForLog: 3.19 MiB -> 0.08 MiB, a 41x reduction, and retention no longer scales with
history length. It stays a clone rather than an alias because body is rewritten downstream
(plugin onRequest hook, compression) and the log must show what the client actually sent.

Bounding at the entry means the logger re-bounds an already-bounded value, which exposed that
cloneBoundedForLog was NOT idempotent -- each container exceeded its own bound once the marker
was added, so a second pass truncated again:

  arrays   [marker, ...24 items] is 25 entries > 24, so the marker and one real item were
           dropped and originalLength was rewritten as 25 instead of the true 729
  objects  80 keys + _omniroute_truncated_keys is 81 > 80, so a real key was evicted to make
           room for the marker and the dropped count was reported as 1 instead of 20
  strings  the marker was appended AFTER slicing to maxLength, so the bounded string was
           longer than the bound

Without this the persisted log payload would have changed shape versus before the fix. All
three now keep the marker inside the budget and treat an already-bounded value as final;
verified end to end -- the marker still reports originalLength 729.

TDD: tests/unit/repro-7847-bound-client-raw-request.test.ts was written first and failed on
three assertions (unbounded retention, retention scaling with history, and the idempotence
precondition) before either change.
2026-07-26 03:52:46 -03:00
MumuTW
4bf47c9b80 chore(perf): add deterministic request-body heap benchmark (#7847) (#8549)
#7847 reports a 3.05 MiB request (729 messages / 86 tools) reaching ~12,282 MiB of V8
heap, and asks for "a regression benchmark that records peak heap for representative
500-800-message, tool-rich requests" before any fix lands. There is currently no memory
baseline in the repo at all (bench:compression is the only benchmark), so a clone-reduction
change could neither be justified nor regression-guarded.

npm run bench:heap-body attributes retained heap to each copy the chat path makes:

  | mechanism                          | call site                          | retained | x wire |
  | cloneLogPayload (unbounded)        | chat.ts buildClientRawRequest      | 3.18 MiB |  1.04x |
  | cloneBoundedForLog (bounded)       | requestLogger.logClientRawRequest  | 0.04 MiB |  0.01x |
  | structuredClone x3 (combo targets) | combo.ts attemptBody               | 9.53 MiB |  3.12x |
  | JSON.stringify (token estimate)    | combo.ts estimateTokens            | 3.06 MiB |  1.00x |
  | per request (sum)                  |                                    |15.81 MiB |  5.17x |

It measures the real production helpers rather than reimplementations, so a change to the
log bounds or the clone strategy is reflected directly.

Design notes:
- Deterministic: fixed-seed LCG, no Math.random(). Verified byte-identical across three
  consecutive runs — without that, a before/after delta measures noise, not the change.
- Corpus lives in its own side-effect-free module so the unit test can import it without
  booting SQLite (requestLogger transitively opens the DB at import time).
- Hermetic: DATA_DIR is redirected to a temp dir before importing, so the benchmark never
  touches the operator's real ~/.omniroute store.
- Node, not bun: --expose-gc and V8 heap accounting are the measurement; another engine's
  heap number would not describe the production runtime.
- --max-retained-mib exits non-zero, so this can become a CI gate once a target is agreed.

Reports only; wires nothing into CI and changes no production code.
2026-07-26 03:52:42 -03:00
MumuTW
1cfbfc044a fix(sse): estimate tokens from the object and count JSON length without building it (#7847) (#8558)
Two changes with one root cause: several hot paths built a full JSON string only to read
its .length, and one of them silently changed the answer.

1. CORRECTNESS -- combo's fallback-compression trigger

   estimateTokens(JSON.stringify(attemptBody)) took the STRING branch of estimateTokens,
   which is ceil(length / CHARS_PER_TOKEN) over the raw JSON. An inline base64 image is
   then charged as if every character of the data URL were prose. Measured on a 200 KB
   inline image:

     via string (before)  50,039 tokens
     via object (after)    1,231 tokens

   a 40x over-count, tripping fallback compression on requests nowhere near the context
   window. This is the same class #8368/#8401 fixed on the request path; the combo call
   site was missed. Passing the object routes through extractImageTokens, which charges
   images structurally. Text-only bodies are unaffected -- verified identical, and pinned
   by a test.

2. ALLOCATION -- jsonLength()

   Adds an exact serialized-length walker: same O(n) scan, no string. Used by
   estimateTokens' object branch and by streamReadinessPolicy (which runs on every
   streaming request and only ever used .length).

   Exactness matters because every consumer feeds a threshold, so this is property-tested
   against JSON.stringify over 4000 generated structures covering escaping, lone
   surrogates, omitted values, non-finite numbers, toJSON, Date, Map, cycles and BigInt.
   Anything outside the plain-JSON subset falls back to JSON.stringify for THAT SUBTREE
   only, so an exotic leaf never forces the message history back onto the allocating path.

Honest scoping of the memory win: the string was always transient, and V8 collects it
efficiently, so this is not 3 MiB of retained heap. Measured allocation churn over 20 calls
on a 3.06 MiB body: 3.1 MiB -> 0.5 MiB, about 6x less. The #8549 benchmark row for this
mechanism measures a HELD string and therefore overstates it; the correctness fix above is
the larger deliverable here.
2026-07-26 03:52:38 -03:00
MumuTW
c06cd83022 fix(sse): shallow per-target copy for the combo attempt body (#7847) (#8553)
* fix(sse): shallow per-target copy for the combo attempt body (#7847)

combo.ts deep-cloned the request body for every target. On a 3.05 MiB agent request that
is 9.53 MiB at 3 targets, and it scales linearly:

  3 targets    9.53 MiB (3.12x wire)  ->  ~0.001 MiB
  5 targets   15.89 MiB (5.19x wire)  ->  ~0.000 MiB
 10 targets   31.78 MiB (10.39x wire) ->  ~0.001 MiB

The isolation it bought only ever needed to contain TOP-LEVEL SCALAR writes. The full
mutation surface on this path is two assignments:

  combo.ts     bodyRecord.max_tokens = ...   (reasoning buffer)
  chatCore.ts  body.model = model            (Background Task Redirection T41)

Nothing mutates the nested payload; applyCompression and injectUniversalHandoffBody both
return new objects (verified empirically -- neither touches its input, and both tolerate a
frozen one). So a fresh top-level object per target gives identical isolation while sharing
the expensive messages/tools arrays.

Also fixes a REAL cross-target leak in handleRoundRobinCombo. It already used a shallow
copy, but took it only when the reasoning buffer actually changed max_tokens -- every other
attempt shared the caller's object outright. The new test reproduces it on the unmodified
code: target 2 received model "mutated-by-openai/gpt-4o-mini". In production that is a
Background Task Redirection on one round-robin target rewriting body.model for the next.
The copy is now unconditional.

The invariant is pinned by tests rather than by a comment listing mutation sites, so the
clone strategy can change again without anyone re-deriving them by hand:
 - a target's in-place write must not leak into the next (priority / fill-first /
   round-robin; the stub reproduces chatCore's body.model write)
 - the caller's body is never mutated
 - the per-target copy stays shallow (targets share one messages array)
 - freeze probe: combo's own body handling performs no in-place writes

* test(sse): register the combo attempt-body isolation test in stryker tap.testFiles

The new test resets the circuit breaker in beforeEach, so it counts as a covering test
for src/shared/utils/circuitBreaker.ts. Without registering it,
check:mutation-test-coverage --strict reported a 4th drift entry that was not there on the
pristine tip -- new drift introduced by this PR. Registered in sorted position; the gate is
back to the 3 pre-existing entries (accountFallback.ts, error.ts, comboPredicates.ts) that
#8538 addresses.
2026-07-26 03:52:35 -03:00
MumuTW
1a7079599c chore(combo): extract pure error predicates and quota status helpers to comboPredicates (#8548) 2026-07-26 03:52:31 -03:00
MumuTW
533f8051a4 chore(token-refresh): decompose services/tokenRefresh.ts into tokenRefresh/* leaves (999 → 724) (#8547)
* chore(token-refresh): extract rotation/cas/circuit-breaker refresh logic into tokenRefresh/* leaves

* test(oauth): follow isUnrecoverableRefreshError to tokenRefresh/shared.ts

cad2c7285 moved isUnrecoverableRefreshError out of tokenRefresh.ts into
tokenRefresh/shared.ts. This suite asserts on source *text* (it regex-matches
the function body to prove the unrecoverable sentinel is returned), so the
move made it fail to find the definition — the only red test across the 23
tokenRefresh-related suites.

Repoint the read() at the file that now defines the body. The public surface
is unchanged: tokenRefresh.ts still re-exports the symbol, verified by import.

* docs(changelog): add fragment for this PR

* docs(auth): correct the #7338 attribution wording in the tokenRefresh header

The header claimed credit for KooshaPari's #7338 was "preserved via co-authorship on the
extraction commits", but none of the commits carries a Co-authored-by trailer -- and adding
one would be inaccurate, since this is an independent implementation against the current
tip rather than a reuse of that diff. The by-name credit for proposing the split stays;
only the false claim about the mechanism is removed.
2026-07-26 03:52:27 -03:00
MumuTW
074fd6de88 chore(validation): decompose providers/validation.ts into validation/* leaves (→ 442) (#8546)
* chore(validation): extract web-cookie, kiro, specialty-inline validators into validation/* leaves

* docs(changelog): add fragment for this PR
2026-07-26 03:52:23 -03:00
MumuTW
f0b08f95c3 chore(usage): decompose services/usage.ts into per-provider usage/* leaves (999 → 253) (#8545)
* chore(usage): extract crof, nanogpt, qoder, opencode, deepseek, bailian, vertex, xiaomi-mimo, xai, github usage fetchers into usage/* leaves

Decompose services/usage.ts (god-file phase 1): move the remaining per-provider
usage fetcher/parser logic into co-located leaves under open-sse/services/usage/
so usage.ts becomes a thin dispatcher (imports + USAGE_FETCHER_PROVIDERS +
getUsageForProvider switch + __testing re-exports).

New leaves (each a pure data transform or independent fetcher, no orchestration):
  - usage/github.ts    getGitHubUsage, formatGitHubQuotaSnapshot, inferGitHubPlanName, shouldDisplayGitHubQuota
  - usage/crof.ts      getCrofUsage
  - usage/nanogpt.ts   getNanoGptUsage
  - usage/qoder.ts     getQoderUsage, parseQoderUserStatusUsage
  - usage/opencode.ts  getOpencodeUsage
  - usage/deepseek.ts  getDeepseekUsage
  - usage/bailian.ts   getBailianCodingPlanUsage
  - usage/vertex.ts    getVertexUsage
  - usage/xiaomi-mimo.ts getXiaomiMimoUsage
  - usage/xai.ts       getXaiUsage

usage.ts re-exports parseQoderUserStatusUsage (named) and threads every helper
the existing __testing contract exposes (usage-utils / usage-service-hardening /
qoder-usage-quota / xiaomi-mimo-selftrack / xai-usage / vertex-spend suites read
them from services/usage). External importers unchanged.

open-sse/services/usage.ts: 1065 -> 256 lines (cap 800).
npm run check:file-size: OK. typecheck:core: OK. eslint: clean. check:cycles: OK.

Added characterization tests (tests/unit/usage-<provider>-split.test.ts) that
import each leaf directly and pin its export surface + key edges, mirroring the
existing usage-quota-core-split / usage-scalars-split pattern.

* docs(changelog): add fragment for this PR
2026-07-26 03:52:19 -03:00
MumuTW
56185a8a49 refactor(dashboard): clear file-size base-red by extracting provider-card highlight wiring (#8524)
* chore(auth): condense #8407 note in tokenHealthCheck to fit frozen file-size baseline

The 3-line comment added by #8426 pushed tokenHealthCheck.ts to 843 LOC,
2 over its frozen baseline of 841 (base-red on release/v3.8.49). Fold the
devin-cli rationale into one line — same meaning, gate green again.

* refactor(dashboard): extract provider card highlight into HighlightableProviderCard

#8349 left the back-navigation highlight concern inline in providers/page.tsx
(state + two callbacks + onCardClick/ref props repeated at 18 card call
sites), pushing the page to 1990 LOC over its frozen baseline of 1927.

Move the concern into a HighlightableProviderCard wrapper: each card instance
keeps its own copy of the highlighted id and only the card whose provider id
matches scrolls into view and highlights — same semantics as the single
page-level state, since resolveHighlightedCard already gates on the id match.

page.tsx drops to 1917 LOC (under the frozen baseline, no rebaseline needed).

* test(dashboard): cover HighlightableProviderCard wiring

Direct coverage for the wrapper extracted in the previous refactor:
mount-time scroll+highlight only when history.state.providerId matches,
no reaction on mismatch/null state, isolation across multiple card
instances, and click-time navigation recording via history.replaceState.

Note: the jsdom scrollIntoView shim is a plain no-op rather than a
vi.fn() — vi.restoreAllMocks() would otherwise restore the shared mock
and let the next test's spy inherit its accumulated call count.
2026-07-26 03:52:15 -03:00
MumuTW
6b5e7d562e fix(devin-cli): update ACP JSON-RPC protocol for Devin CLI 3000.2.x (Fixes #8406) (#8425)
* fix(executors): fix ACP protocol wire format for devin cli

* test(sse): replace no-explicit-any with AcpFrame type in devin-cli ACP test

no-explicit-any is a hard error in tests/; type the parsed JSON-RPC frames
with a local AcpFrame shape instead of (f: any) callbacks, and apply
prettier formatting to the same file.

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-26 03:52:12 -03:00
backryun
11f79b65cd refactor(sse): stop isClaudeEventPayload claiming a narrowing it never performs (#8557)
Eight TS2339s in stream.ts read properties off `never`. `never` here did not
mean unreachable code — it meant a type predicate was lying.

  function isClaudeEventPayload(payload: unknown): payload is JsonRecord
  ...
  const flushedParsed = bufferedPayload as JsonRecord;
  const isClaude = isClaudeEventPayload(flushedParsed);
  } else if (!isClaude) {   // JsonRecord minus JsonRecord = never

The predicate answers "does this payload carry a Claude event type?" — a
question about contents, not type. A Claude event and a non-Claude one are both
JsonRecord, so `payload is JsonRecord` narrows nothing; applied to an argument
already typed JsonRecord it collapsed the negative branch to `never`, taking
`.id` and `.choices` with it.

Changed the return type to `boolean`. No call site depended on the narrowing:
both other callers pass the value to updateClaudeEmptyResponseLifecycle(), whose
parameter is `unknown`, and passthroughTailProcessor.ts already declares this
function as `(payload: unknown) => boolean` — so this aligns the definition with
how the codebase already consumes it.

280 -> 272, zero new, on a line-number-agnostic diff of the full tsc error set.

The diff is one line, and a type predicate has no runtime representation, so the
emitted JavaScript is unchanged.

No test added, and no gap to fill: the `never`-typed branch is real, reachable
and already covered from both sides — stream-numeric-ids.test.ts exercises
"normalizes numeric id in final chunk without trailing newline" (the flush path
with a non-Claude payload, i.e. the branch TS believed impossible) and "Claude
passthrough does not normalize numeric ids" (the other arm). 338/338 across the
48 SSE/passthrough suites.

No explanatory comment either: stream.ts sits exactly at its frozen file-size
cap (2887), so a single added line fails check:file-size. The reasoning lives
here and in the PR rather than costing a baseline bump on a frozen file.

Co-authored-by: backryun <busan011@ormbiz.co.kr>
2026-07-26 03:52:08 -03:00
backryun
647afe327b refactor(sse): declare the multipart/gRPC frame bodies as ArrayBuffer-backed (#8533)
* refactor(sse): declare the multipart/gRPC frame bodies as ArrayBuffer-backed

Seven TS2769 across three files, one root cause: a bare `Uint8Array` widens to
`Uint8Array<ArrayBufferLike>`, which admits `SharedArrayBuffer` and so is not
assignable to `BodyInit`. Every one of the seven is a `fetch({ body })` call
rejecting an otherwise-valid payload.

They flow from exactly two producers:

  buildMultipartBody()  audioTranscription.ts  -> 5 sites here + 1 in audioTranslation.ts
  grpcWebFrame()        windsurf.ts            -> 1 site

Both allocate with `new Uint8Array(length)`, which is always ArrayBuffer-backed,
so declaring `Uint8Array<ArrayBuffer>` states what the code already guarantees.
This is a narrowing of the declaration to match reality, not a cast at the call
sites — no `as BodyInit` anywhere, and the seven call sites are untouched.

280 -> 273, zero new, on a line-number-agnostic diff of the full tsc error set.
The runtime diff is two return-type annotations; nothing executable changed.

Tests: buildMultipartBody already has four direct tests, but they assert the
payload's *contents* (boundary, filename sanitization, MIME fallback) — none
assert the backing, which is precisely what the new type guarantees and what a
plausible switch to a pooled `Buffer.concat` would break. Added
multipart-body-arraybuffer-backing.test.ts (3 tests): the buffer is a plain
ArrayBuffer, the view spans it entirely at offset 0 (no pool remainder), and
both hold for a 64 KiB payload past Node's Buffer-pool threshold. 258/258 across
the 17 affected audio/windsurf suites.

Not included: the 3 remaining TS2339 in audioTranscription.ts (`data?.data?.…`
on an untyped poll result). Different root cause — grouped by kind, not by file,
per #8484.

* chore(quality): trim the buildMultipartBody doc so audioTranscription.ts stays under the 800 cap

My doc comment on buildMultipartBody added 9 lines to a file with 8 to spare
(792 -> 801 as check:file-size counts, cap 800), so this PR was failing the gate
on growth I introduced. Condensed the comment to 4 lines; the file lands at 796.

Rebuilt on top of the /green-prs sync merge (9973bf3bf) rather than force-pushing
my own rebase over it — the release-tip sync there is @ikelvingo's work and had
already been validated against this branch.

Delta unchanged: 280 -> 273, 7 fixed, zero new. 68/68 on the audio/windsurf
suites; typecheck:core and eslint clean; check:file-size now OK.

---------

Co-authored-by: backryun <busan011@ormbiz.co.kr>
Co-authored-by: ikelvingo <im.kelvinwong@gmail.com>
2026-07-26 03:52:04 -03:00
backryun
d628105503 refactor(sse): retag the designer-web result unions with string discriminants (#8531)
All 10 diagnostics in this file are the `strictNullChecks: false` narrowing
limitation: a boolean-literal discriminant narrows the positive branch but
leaves the negative one as the full union, so `if (!resolved.ok)` and the code
after `if (outcome.success)` could not see `status`/`error`.

Three unions, all module-private and untouched by any test, so per the rule
recorded on #8499 these are retagged rather than fixed with predicates —
predicates are for exported unions whose shape callers depend on:

  resolveDesignerWebRequest  ok: true|false      -> state: "resolved"|"invalid"
  DesignerWebStepResult      done + success      -> state: "pending"|"ready"|"failed"

The step union collapsed two booleans into one discriminant; `done`/`success`
encoded three states across two flags, which is also why the pending arm had no
`success` property for `outcome.success` to read.

Also narrowed runDesignerWebPollLoop's declared return from
`DesignerWebStepResult | {…504…}` to a new DesignerWebOutcome (ready | failed).
The loop returns a step only after confirming it is terminal, and otherwise
synthesizes a 504 — it can never return a pending step, and the old signature
claiming it could is what made `.success` unreadable on the union at all.

280 -> 270, zero new, on a line-number-agnostic diff of the full tsc error set.

Tests: unlike the previous slices this rewrote real control flow (three
conditionals), so the existing suite is doing actual work here —
microsoft-designer-web-6672.test.ts drives the handler end-to-end through 400,
401, immediate-ready, poll-then-ready, non-OK upstream and 504-timeout, i.e.
every arm but one. The "empty" arm (unrecognized 200 body -> terminal 502) was
tested only at the parser level, never through the handler, so the 502 itself
was unasserted. Added designer-web-empty-response-502.test.ts (2 tests) pinning
that it is terminal (exactly one fetch, no polling) and distinct from the 504
deadline path. Both suites pass against the parent commit too — the tests are
black-box through the exported handler, so they are agnostic to the discriminant
rename and prove the retag is behavior-preserving. 61/61 across the 3 suites.

Co-authored-by: backryun <busan011@ormbiz.co.kr>
2026-07-26 03:52:00 -03:00
backryun
2cf462c1a1 refactor(sse): type the rerank response adapter's options parameter (#8528)
`transformResponseFromProvider(providerConfig, data, options = {})` annotated
nothing, so TS inferred `options` as `{}` from its default value and rejected
every read of it: `top_n` x6, `documents` x4, `return_documents` x2 across the
DeepInfra and Voyage adapters. All 12 of the file's diagnostics, one cause.

Declared `RerankResponseOptions` from the JSDoc that already documents these
fields on handleRerank, with `documents` as `Array<string | { text?: string }>`
— the two forms the Cohere-compatible API accepts and the adapters already
branch on. `providerConfig` and `data` stay unannotated; only `options` was
producing errors and only `options` is touched.

280 -> 268, zero new, on a line-number-agnostic diff of the full tsc error set.

Tests: `{ text }` documents were only ever exercised through the *request*
adapter (#5332, #7809) — every response-adapter test passed plain strings, so
the `typeof doc === "string" ? doc : doc?.text` branch on the response side was
uncovered, and that branch is exactly what gives the declared type its union.
Added rerank-object-documents-response-path.test.ts (5 tests): object-form
documents resolved through both adapters, a mixed string/object array, the
`{}`-without-text fallback, and Voyage's index remap across an empty `{ text: "" }`
document. They pass against the parent commit's rerank.ts too — no behavior
change. 43/43 across the 6 rerank/media-cost suites.

Co-authored-by: backryun <busan011@ormbiz.co.kr>
2026-07-26 03:51:56 -03:00
backryun
0312fe2a40 refactor(guardrails): narrow the documented void return at the dispatch site (#8525)
`BaseGuardrail.preCall`/`postCall` return `GuardrailResult<unknown> | void`,
and that `| void` is deliberate: docs/security/GUARDRAILS.md documents returning
nothing as one of the three "no change" signals, and CredentialMaskerGuardrail
still declares it. So the union stays.

The problem is the consumption site. `registry.ts` reads `result?.block`,
`result?.message`, `result?.meta`, `result?.modifiedPayload` — but optional
chaining does not make `void` inspectable, and neither does a truthiness test.
That is all 12 TS2339s in the file: one union, six property reads, two dispatch
loops.

Fixed by funnelling the return through `unknown` once, in a local
`asGuardrailResult()` helper, so the loops work against a plain
`GuardrailResult | undefined`. The public contract is untouched — no signature
narrowed, no guardrail changed, callers outside this file unaffected.

280 -> 268, zero new, on a line-number-agnostic diff of the full tsc error set.

Tests: the `void` arm of the documented contract had NO coverage —
guardrails-registry.test.ts exercises guardrails that return a result and one
that throws, never one that returns nothing. Added
guardrails-void-no-change-contract.test.ts (4 tests): silent preCall/postCall
pass through untouched and are recorded as ran-not-skipped-not-errored;
returning `{}` produces an identical execution record; a silent guardrail does
not stop a later one from modifying. They pass against the parent commit's
registry.ts too, which is the evidence for no behavior change.

75/75 across the 13 guardrail suites.

Co-authored-by: backryun <busan011@ormbiz.co.kr>
2026-07-26 03:51:53 -03:00
backryun
fd739ab008 refactor(sse): restore three executor types the runtime already relied on (#8520)
Three independent root causes in the TS 7 executor slice (#8484), each a
declaration that had drifted behind the code using it. All type-only —
no behavior change, verified by running the new tests against the parent
commit's sources (7/7 pass there too).

mimocode — AccountState was missing `proxy`, but syncAccountsFromCredentials()
writes it on every account and getProxyDispatcher() reads it. The #3837/#5521
contract ("always present, null when unconfigured") lived only in a comment.
Declared as AccountProxyConfig["proxy"] so the two stay in lockstep. (4)

opencode — the tools-truncation block narrowed `modifiedBody` with
`typeof === "object"` but, unlike the client_metadata block directly above it,
omitted `!Array.isArray()` and the Record cast, so `.tools` was unreachable on
`object`. Adopted the neighbouring block's idiom. Behavior is identical: the
old code reached `.tools` on arrays too and relied on Array.isArray(undefined)
short-circuiting. (4)

zed-hosted — enqueueSseObject/finish/processLine were annotated
ReadableStreamDefaultController but are driven from a TransformStream, whose
controller has no close(). All three only ever enqueue, so they are now typed
by that single capability (SseEnqueueTarget). (3)

310 -> 299 diagnostics, zero new, on a line-number-agnostic diff of the full
tsc error set.

Tests: new tests/unit/ts7-executor-shared-shapes.test.ts pins the tools
truncation (previously uncovered) and the proxy-always-present contract. The
zed-hosted transform is already covered end-to-end by
zed-hosted-think-close-marker.test.ts. 300/300 across the 26 affected files.

Co-authored-by: backryun <busan011@ormbiz.co.kr>
2026-07-26 03:51:49 -03:00
backryun
1bd1af2f48 refactor(sse): narrow three result unions via type predicates (#8499)
* refactor(sse): narrow three result unions via type predicates

Eight diagnostics across three executors, all the same root cause already recorded in
#8483: this workspace compiles with `strictNullChecks: false`, where a boolean-literal
discriminant narrows the positive branch but not the negative one. Reading a
failure-only field after `!result.ok` therefore leaves the full union.

  auggie.ts        2  .error       on AuggieModelResolution
  muse-spark-web   4  .error       on GraphqlResult
  notion-web       2  .retryable / .errorResult on the runOnce union

#8483 retagged its union with a string discriminant. That is the better shape when the
union is module-private and small, but it does not fit here: `resolveAuggieModel` is
exported and its tests deep-equal the literal `{ ok: true, model }` object, so retagging
would churn public API and assertions to fix a checker limitation. Each union instead
gets an explicit type predicate, which narrows correctly under these compiler settings
while leaving the shape, every call site, and the tests untouched. notion-web's inline
union is named `NotionAttempt` first so it has something to `Extract` from.

Validation: full tsc error-set diff against the base config — 335 -> 327, zero new
errors (line-number-agnostic). `typecheck:core` clean; the 6 existing test files
importing a touched executor pass.

Coverage: each predicate is a one-liner whose control flow inverts on a stray `!`, and
all three failure branches already have behavioral guards —
`auggie-executor.test.ts` (400 + /Unknown Auggie model/),
`muse-spark-web-continuation.test.ts` ("Warmup failed: …"), and
`executor-notion-web.test.ts` (nested temporarily-unavailable → retried). The two
assertions added here pin the arm that the predicate unlocks on the one union that is
exported and directly reachable.

* chore(quality): rebaseline muse-spark-web.ts for #8499 own growth

The new isGraphqlFailure() type-predicate helper (TS7 strictNullChecks:false
narrowing fix) grows the frozen file 1396->1405 (+9), irreducible per the
justification recorded in config/quality/file-size-baseline.json.

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>

---------

Co-authored-by: ikelvingo <im.kelvinwong@gmail.com>
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-07-26 03:51:45 -03:00
backryun
06326a3c80 refactor(sse): stop three executors shadowing BaseExecutor.buildHeaders (#8498)
`BaseExecutor.buildHeaders(credentials, stream?, clientHeaders?, model?, health?)` was
shadowed in three executors by same-named helpers with unrelated signatures:

  hailuo-web  private   buildHeaders(token: string, yy: string)
  lmarena     protected buildHeaders(_model: string, credentials: unknown, _body: unknown)
  qwen-web    private   buildHeaders(token: string, cookieHeader: string, chatId?: string)

Name collisions, not overrides — each reported TS2416. They are renamed to
`buildStreamHeaders` / `buildRequestHeaders` / `buildApiHeaders`; the two lmarena test
files that called the helper directly are updated with them.

Worth stating precisely, because the shadow sat on a live dispatch path without being a
live bug: `BaseExecutor.countTokens()` calls `this.buildHeaders(credentials, false)`, and
all three inherit `countTokens()`. It is unreachable today only because
`buildCountTokensUrl()` returns null unless `config.format === "claude"` and the URL
carries `/messages` — hailuo-web and qwen-web set no format, lmarena sets `"openai"` — so
`countTokens()` returns at the guard above. Latent, not live; one `format` change away
from passing a credentials object where a token string is expected.

Two more, surfaced by clearing the above:

* `lmarena` declared `buildUrl` and `transformRequest` `protected` while both are public
  on BaseExecutor (TS2415 — a subclass may widen visibility, never narrow it). Both were
  masked behind the buildHeaders TS2416 and appeared one at a time as it cleared. Runtime
  is unaffected; JavaScript has no member visibility.

* `GithubExecutor.refreshCredentials` had no declared return type, so TypeScript inferred
  the union of its four literal returns. `GheCopilotExecutor` legitimately overrides it
  with a wider `providerSpecificData` (it also records the enterprise proxy URL) and no
  `expiresIn`, which is not assignable to that inferred union. Declared as
  `RefreshedCopilotCredentials | null` — same shape of fix as #8489, on a different method.

Validation: full tsc error-set diff against the base config — 335 -> 331, zero new errors
(line-number-agnostic). `typecheck:core` clean; the 15 existing test files importing a
touched executor pass, including lmarena's 44 across the two updated files.
`plan3-p0.test.ts` fails identically with and without this change (it reads the
developer's real ~/.omniroute DB rather than a test-scoped DATA_DIR).

The new test pins that the inherited method is no longer shadowed — verified to fail on
the base, where all three prototypes still carry their own `buildHeaders` — and that the
`countTokens()` early return which kept it harmless still holds.
2026-07-26 03:51:42 -03:00
AmirHossein Rezaei
a51cd06322 fix(resilience): honor comboCooldownWait for every combo strategy (#8541) (#8559)
* fix(resilience): honor comboCooldownWait for every combo strategy

The cooldown-wait path claimed all strategies, but isComboCooldownWaitEligible still gated on quota-share/auto. Widen the gate, raise the per-target timeout floor with it, and consult the allow-list from earliestRetryAfter so a later 403 cannot skip the decision. Fixes #8541.

* test(combo): fold cooldown-wait strategy assertions into a loop

Keep combo-config.test.ts under the file-size ceiling while covering every
routing strategy (including internal quota-share) for the #8541 gate fix.

* chore(combo): trim cooldown-wait comment under file-size ceiling

After rebase onto tip the net +2 in combo.ts sat one line over the frozen
check:file-size count (split-based); fold the SECURITY note into the block.
2026-07-26 03:51:38 -03:00
ikelvingo
f60c9fe542 chore(quality): rebaseline complexity/cognitive for v3.8.49 merge-train own-growth 2026-07-25 21:46:36 -03:00
ikelvingo
1d7878d857 chore(quality): rebaseline complexity/cognitive to v3.8.49 tip drift 2026-07-25 21:03:23 -03:00
Alex
4053e2314a chore(quality): rebaseline file-size for inherited base growth (#8561)
check:file-size fails on the pristine release/v3.8.49 tip (30709255),
which takes the whole Fast Quality Gates job down for every PR against
the branch. Both offenders grew after the last rebaseline
(_rebaseline_2026_07_25_v3849_basered_filesize, measured at 36f8fd10)
and both came from already-merged PRs, so there is no offending branch
left to fix:

  src/lib/tokenHealthCheck.ts                     841 -> 843  (#8426, 4528fc455)
  src/app/(dashboard)/dashboard/providers/page.tsx 1927 -> 1990 (#8349, 58ab8b1d2)

Trust-but-verify: both values measured on the pristine tip with an empty
working tree. Same situation and remedy as the entry cited above and as
_rebaseline_2026_07_02_5798_release_green.

This commit is deliberately last and self-contained: drop it if the
captain would rather move the frozen values separately. Structural
reduction of the 1990-line providers page is not attempted here.

Co-authored-by: alexey.nazarov@softmg.ru <alexey.nazarov@softmg.ru>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 11:25:14 -03:00
MumuTW
cc63ac9f53 test(sse): register #8396 and #8376 unit tests in stryker tap.testFiles (#8538)
* test(sse): register #8396 and #8376 unit tests in stryker tap.testFiles

check:mutation-test-coverage fails on release/v3.8.49 at its own HEAD:
two unit tests cover mutated modules but are absent from tap.testFiles,
so their mutant kills do not count and the drift gate blocks every
PR->release run.

- tests/unit/8396-cooldown-429-cap.test.ts covers
  open-sse/services/accountFallback.ts (imports checkFallbackError)
- tests/unit/8376-econnrefused-breaker.test.ts covers
  open-sse/services/combo/comboPredicates.ts (imports
  shouldRecordProviderBreakerFailure)

Both inserted in the array's existing sorted position; no other key
touched.

* test(sse): register repro-7503-no-choices in stryker tap.testFiles
2026-07-25 11:10:52 -03:00
MumuTW
c447be4329 fix(db): classify compressionDetailNormalizers as db-internal in check-db-rules (#8534)
* fix(db): classify compressionDetailNormalizers as db-internal in check-db-rules

check:db-rules fails on release/v3.8.49 at its own HEAD: the module added
by #8404 is neither re-exported from localDb.ts nor listed in
INTENTIONALLY_INTERNAL, so the gate blocks every PR->release run and
tests/unit/check-db-rules.test.ts fails its live-repo case.

Its only importer is its sibling src/lib/db/compression.ts, via a
relative import inside src/lib/db/ — the db-internal classification the
list already uses for apiKeyColumnFallbacks and caseMapping. Re-exporting
it from localDb.ts would instead advertise pure normalizer helpers as
part of the compat surface, which Hard Rule #2 discourages.

* test(db): mirror compressionDetailNormalizers in the INTENTIONALLY_INTERNAL audit

The classification guard asserts the exact audited set. Adding the module to
check-db-rules.mjs without the mirror left the exact-list/exact-size assertion
red; both assertions stay exact (37 entries).

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>

---------

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-07-25 11:10:48 -03:00
MumuTW
d06d3fb67d test(sse): update three backoff assertions stale since the #8396 cooldown cap (#8539)
These three cases assert that a transient-error cooldown keeps doubling
to baseCooldownMs * 2^maxLevel — roughly 45.5h at the default constants.
That is precisely the blackout #8396 removed: capScaledCooldownMs
(open-sse/services/accountFallback/cooldownCap.ts) now bounds every
scaled cooldown by profile.maxCooldownMs, falling back to
BACKOFF_CONFIG.max when the profile does not configure one. All three
call checkFallbackError with a null provider, so the fallback ceiling
applies and the observed value is BACKOFF_CONFIG.max.

The backoff-level clamping each case was written to guard is unchanged
and still asserted; only the expected duration moved. The expressions
keep the original formula wrapped in the cap so the relationship stays
readable, and the first case gains a precondition assertion so it cannot
silently become vacuous if the constants change.

Fixes the error-classification (x2) and thundering-herd (x1) failures
that are red on release/v3.8.49 at its own HEAD.
2026-07-25 11:10:44 -03:00
MumuTW
278640b438 chore(ci): resync stale no-explicit-any suppression count for proxy-registry.test.ts (#8544)
* chore(ci): resync stale no-explicit-any suppression count for proxy-registry.test.ts

tests/unit/proxy-registry.test.ts is frozen at 55 no-explicit-any
violations but only has 54 since #8447 (d7f947586) removed one. ESLint
fails the run with "There are suppressions left that do not occur
anymore", making `npm run lint` exit 2 on release/v3.8.49 for every PR
that branches off it.

Same class as #8007 / #8490, different file: no code to fix here — the
count simply drifted down, so this resyncs it via --prune-suppressions.

Base-red inherited from release/v3.8.49; both the suppressions file and
proxy-registry.test.ts are byte-identical to that branch.

* docs(changelog): add fragment for this PR
2026-07-25 11:10:33 -03:00
Diego Rodrigues de Sa e Souza
ec0be07682 chore(deps): patch js-yaml + postcss for 2 high Dependabot alerts (#8572)
js-yaml 5.2.1 -> 5.2.2 (GHSA-pm4m-ph32-ghv5, CWE-407: exponential parsing time in flow collections — a <200-byte payload hangs load()).
postcss 8.5.14 -> 8.5.23 (GHSA-r28c-9q8g-f849, CWE-22: path traversal via auto-loaded sourceMappingURL discloses arbitrary .map files).

Scoped override keeps promptfoo off its exact js-yaml@5.2.1 pin while holding
@apidevtools/json-schema-ref-parser on js-yaml ^4.2.0, so the nested override does
not major that subtree. Lockfile diff is exactly three versions (plus postcss's own
nanoid); nothing added or removed. check:lockfile passes.

Closes Dependabot #146 and #148.
2026-07-25 10:35:05 -03:00
Diego Rodrigues de Sa e Souza
30709255c9 fix(sse): stop combo's aggregated failure response from mixing fields across targets (#8486) (#8508)
handleComboChat/handleRoundRobinCombo tracked lastStatus (first-write-wins),
lastError (last-write-wins), and earliestRetryAfter (global MIN across all
targets) independently, so the final unavailableResponse() could surface a
status/message pair from two different failing targets and decorate a
config-class error (e.g. Antigravity's 422 missing_project_id, which carries
no retryAfter of its own) with an unrelated target's long reset window.

- lastStatus now overwrites on every failure (last-write-wins), matching
  lastError, so status and message always come from the same target.
- the "(reset after ...)" decoration is only applied when the surfaced
  status is itself rate-limit-class (429/503) — see the new
  open-sse/services/combo/unavailableRetryGate.ts leaf module (both
  combo.ts and chat.ts are already over their file-size baseline, so the
  gate logic lives in a new module and combo.ts only wires it in).

Co-authored-by: ikelvingo <im.kelvinwong@gmail.com>
2026-07-25 04:57:29 -03:00
Diego Rodrigues de Sa e Souza
9ced2e99df fix(sse): stop stream readiness from treating a choices-less error frame as success (#7503) (#8504)
Co-authored-by: ikelvingo <im.kelvinwong@gmail.com>
2026-07-25 04:57:22 -03:00
Diego Rodrigues de Sa e Souza
6cad24eec0 fix(api): probe provider alias in models.dev reverse capability lookup (#8429) (#8506)
reverseModelsDevProviders() only matched MODELS_DEV_PROVIDER_MAP entries
by the canonical OmniRoute provider id, but the map's RHS for the OAuth
CLI providers (codex/claude) only lists their alias (cx/cc), never the
canonical id itself. Since the models.dev sync job writes
model_capabilities rows under openai/cx and anthropic/cc (never
codex/claude), and the auto-combo gate canonicalizes a codex/... or
claude/... target's provider to "codex"/"claude" before the lookup,
the synced capability row was unreachable for those two providers.

Also probe the provider's alias (via the already-imported
PROVIDER_ID_TO_ALIAS) when scanning the map, so a canonical id like
"codex"/"claude" still matches entries keyed only by their alias.

Co-authored-by: ikelvingo <im.kelvinwong@gmail.com>
2026-07-25 04:57:15 -03:00
Diego Rodrigues de Sa e Souza
61277813b2 fix(backend): compute AgentBridge diagnose DNS check per-agent instead of hard-coded Antigravity (#8466) (#8502)
getMitmStatus() hard-wired dnsConfigured to a single Antigravity hostname
regex regardless of which agent was being diagnosed, and the diagnose route
never accepted an agentId to check against. Add checkDNSEntryForAgent()
reusing resolveHostsForAgent()'s existing per-target host resolution, thread
an optional agentId through getMitmStatus(), and have the diagnose route
parse ?agentId= and pass it through. Callers that omit agentId keep the
legacy Antigravity-only behavior unchanged.

Co-authored-by: ikelvingo <im.kelvinwong@gmail.com>
2026-07-25 04:57:07 -03:00
Diego Rodrigues de Sa e Souza
13cc128dae fix(cli): fall back to node:sqlite when better-sqlite3 is unavailable in omniroute doctor (#7586) (#8501)
bin/cli/sqlite.mjs::loadSqlite() had no fallback beyond better-sqlite3, unlike
the real server's driver cascade (src/lib/db/adapters/driverFactory.ts::tryOpenSync,
which tries bun:sqlite -> better-sqlite3 -> node:sqlite). On machines without a
working better-sqlite3 native binary, every `omniroute doctor` DB check reported
a false FAIL even when the actual server was healthy via its own driver cascade.

openSqliteDatabase() now falls back to tryOpenSync() when better-sqlite3 fails
to import, reusing the same already-tested cascade the real server uses instead
of re-deriving a second one.

Co-authored-by: ikelvingo <im.kelvinwong@gmail.com>
2026-07-25 04:57:00 -03:00
Diego Rodrigues de Sa e Souza
55ff236023 fix(providers): repoint zai-web executor to chat.z.ai v2 chat-completions endpoint (#8014) (#8503)
Co-authored-by: ikelvingo <im.kelvinwong@gmail.com>
2026-07-25 04:56:53 -03:00
Diego Rodrigues de Sa e Souza
24142a8e91 fix(providers): expose base-URL override for Kimi/Moonshot CN-region keys (#7447) (#8500)
CN-region Moonshot/Kimi API keys (issued on the domestic
platform.kimi.com/moonshot.cn account) belong to a completely separate
keyspace than the international platform.kimi.ai/api.moonshot.ai
account, so OmniRoute's hard-coded international base URL rejects them
with a generic "Invalid API key" 401.

Neither "kimi" (legacy id) nor "moonshot" (current user-facing id) was
in CONFIGURABLE_BASE_URL_PROVIDERS, so the Add-connection modal never
rendered a base-URL field for them and there was no supported way to
point a new connection at api.moonshot.cn. The underlying
resolveBaseUrl()/buildUrl() primitives already honor a
providerSpecificData.baseUrl override generically (same mechanism used
by siliconflow, xiaomi-mimo, etc.) -- this only exposes that existing
affordance for kimi/moonshot, defaulting to the unchanged
international host so existing users see no behavior change.

Co-authored-by: ikelvingo <im.kelvinwong@gmail.com>
2026-07-25 04:56:46 -03:00
Diego Rodrigues de Sa e Souza
e0aef4deb9 fix(translator): set status:completed on Responses input items to satisfy strict upstream validators (#8083) (#8507)
Co-authored-by: ikelvingo <im.kelvinwong@gmail.com>
2026-07-25 04:56:33 -03:00
backryun
7a8f9156da test(sse): repair two base-red gates on release/v3.8.49 (#8490)
* test(sse): repair two base-red gates on release/v3.8.49

`release/v3.8.49` is red at its own HEAD (36f8fd10) — `Quality Gates` and
`Release-Green (continuous)` both fail — so every PR opened against it inherits
four failing checks regardless of content. Two of those causes had no owner:
#8480/#8481/#8482 cover the compression ladder, the antigravity catalog, and the
resilience/translator regressions respectively, none of them these.

1. `chatcore-client-usage-buffer.test.ts` — 5/5 failing.

   #8331/#8356 (e8719783e) inserted an `options` parameter between
   `clientResponseFormat` and `deps` on `applyClientUsageBuffer()`. The five call
   sites here still passed `deps` in the fourth position, so the injected spies
   landed in the `options` slot and the real implementations ran — every
   `calls.*.length` assertion saw 0. The `Parameters<typeof
   applyClientUsageBuffer>[3]` cast in `makeDeps()` masked the type error, which is
   why it reached the branch. Call sites updated to `(resp, body, format, {}, deps)`
   and the cast repointed to `[4]`.

   Two cases added for the parameter that caused this, since nothing at this layer
   exercised it: `preserveContextBudgetInVisibleUsage` re-folds `context_budget_*`
   into the visible fields for the Claude-Code path, and the default path keeps the
   real unbuffered #8331 numbers.

2. `claude-to-openai-think-close-5123.test.ts` — 4 unsuppressed
   `@typescript-eslint/no-explicit-any` errors, failing `npm run lint`.

   Its suppression entry allows `count: 2` but the file had grown to four `(chunk:
   any)` callbacks. Rather than raise the frozen count, the cause is fixed:
   `collectChunks()` returns `StreamChunk[]` instead of `unknown[]`, narrowing once
   at the boundary so all four callbacks need no annotation. The suppression entry
   is then stale and removed, which ratchets the file to zero.

Test-only plus one allowlist deletion; no production code. Verified on a clean
worktree of the base commit: `npm run lint` clean (was 4 errors),
`chatcore-client-usage-buffer` 7/7 (was 0/5), `claude-to-openai-think-close-5123`
3/3.

* chore(ci): rebaseline five inherited file-size overages on release/v3.8.49

`check:file-size` fails on release/v3.8.49 at its own HEAD (36f8fd10), which is
what turns `Fast Quality Gates` red for every PR against this base:

  tokenHealthCheck.ts   832 -> 841
  chat.ts              1865 -> 1866
  auth.ts              2475 -> 2486
  accountFallback.ts   1941 -> 1960
  combo.ts             3630 -> 3642

None of these files is touched by this PR. The growth was inherited from already
merged PRs that did not bump their entries, so there is no offending branch left
to fix — the same situation the baseline already records under
`_rebaseline_2026_07_02_5798_release_green`, and the procedure it documents is to
raise the frozen values with a justification note.

Raised to the current base values only. The files stay frozen and cannot grow
further; an in-flight PR that adds lines to them bumps its own entry as usual
(#8482 touches accountFallback.ts and combo.ts and will need that).

* chore(ci): register 11 covering unit tests in stryker tap.testFiles

`check:mutation-test-coverage --strict` fails on release/v3.8.49 at its own HEAD:
11 unit tests that cover mutated modules are absent from `stryker.conf.json`
`tap.testFiles`, so their mutant kills do not count. The gate does not self-heal —
it names the exact files to add.

  open-sse/services/accountFallback.ts  + 8247-accountfallback-model-unhealthy,
                                          8248-accountfallback-nvidia-degraded,
                                          model-lockout-exact-cooldown-cap,
                                          repro-antigravity-404-family-cooldown-hijack
  src/sse/services/auth.ts              + 7993-noauth-proxy-routing,
                                          8200-perplexity-web-401-cooldown,
                                          sse-auth-antigravity-credits
  src/server/authz/routeGuard.ts        + authz/route-guard-vnc-session-local-only
  open-sse/utils/error.ts               + error-sensitive-redaction
  open-sse/utils/publicCreds.ts         + adobe-firefly
  src/shared/utils/circuitBreaker.ts    + 8332-combo-vision-fallback

None is a file this PR touches, and the gate reports the identical 11 on a worktree
carrying none of these base-red fixes. Additions only (11 insertions, 0 deletions);
the array stays sorted. This widens what the mutation run accounts for rather than
relaxing anything.

* chore(ci): re-measure accountFallback.ts file-size cap against the current base tip

The entry frozen in this PR (1960) was the value at 36f8fd10; the base has
since advanced to 1cafd328c and the file is 1966 there, so check:file-size
would still have been red on the merge commit. Re-measured to 1966.

Same inherited drift the note already documents: check:file-size does not run
on the PR->release fast path, so growth accrues unmeasured between release
rebaselines. The other four entries still match the current tip
(tokenHealthCheck 841, chat 1866, auth 2486, combo frozen 3642 >= 3640).

---------

Co-authored-by: backryun <busan011@ormbiz.co.kr>
2026-07-25 02:53:25 -03:00
backryun
5a20314782 refactor(sse): declare the executor execute() result contract (#8489)
`normalizeExecutorResult()` has always accepted `Response | { response, url, headers,
transformedBody }` — the bare arm is what the web/scraping executors return from their
error and passthrough paths, and `chatcore-upstream-timeouts.test.ts` already covers
that both shapes are handled. But `BaseExecutor.execute` has no explicit return type,
so TypeScript inferred it from the method's single `return` — the object shape alone.

Every override returning a bare `Response` was therefore reported as incompatible:

  * 14 × TS2739 in `duckduckgo-web.ts`, whose `execute()` additionally pinned its own
    signature to just the object shape while returning `errorResponse()` /
    `processResponse()` (both `Response`) from 14 valid paths
  * TS2416 in `felo-web.ts` and `gitlab.ts`, which declare `Promise<Response>`

Fix the declaration rather than the call sites: export `ExecutorExecuteResult` from
`base.ts` — the same union `normalizeExecutorResult()` accepts — and annotate
`BaseExecutor.execute` with it. `duckduckgo-web.ts` then drops its over-narrow
annotation, matching BaseExecutor and the ~38 other executors that let the return type
be inferred.

Two subclasses read `.response` straight off `super.execute()` and now narrow first:

  * `github.ts` — the existing `!result.response` guard already meant "bare Response,
    nothing to materialize"; it is now expressed as `result instanceof Response`, which
    is the same branch for every input (bare / object / nullish)
  * `pollinations.ts` — reads the status through both arms for its pool bookkeeping

Wrapping DuckDuckGo's 14 returns would have been the wrong fix: the values are already
correct, and `normalizeExecutorResult()` produces exactly `{ response, url: "",
headers: {}, transformedBody: null }` for them.

Validation: full tsc error-set diff against the base config — 335 -> 319, **zero new
errors** (line-number-agnostic diff is empty; the two `duckduckgo-web.ts` TS2345s that
appear to move are the same two pre-existing errors renumbered by added comments, and
are left for a later slice). `typecheck:core` clean, `check:type-coverage` 92.17% ->
94.17%, and 49 of the 50 existing test files importing a touched executor pass —
`plan3-p0.test.ts` fails identically with and without this change (it reads the
developer's real ~/.omniroute DB rather than a test-scoped DATA_DIR).

The new test pins the runtime behavior of the narrowing so a later simplification
cannot quietly drop the bare-Response arm.
2026-07-25 02:53:19 -03:00
backryun
3f2bf86c3c fix(sse): call the real abort-signal helper in the Gemini Business executor (#8485)
`gemini-business.ts` built its upstream fetch options with `combineAbortSignals(...)`,
which is defined nowhere in the repository. The module imports `mergeAbortSignals`
from `./base.ts` on line 31 and never used it — a rename that was only half applied.

Because the call sits inside the fetch options object literal, the ReferenceError was
thrown while *constructing* the arguments, before `fetch()` ran, and the surrounding
try/catch turned it into `makeErrorResult(502, "Gemini Business network error: ...")`.
So every Gemini Business request failed with what reads like an upstream outage. The
provider is registered and reachable (`open-sse/executors/index.ts`), so this affects
the whole provider, not an edge case.

`mergeAbortSignals(primary, secondary)` requires two real signals while
`ExecuteInput.signal` is `AbortSignal | null | undefined`, so the call is guarded and
falls back to the timeout alone — the same shape huggingchat, grok-web, claude-web,
and ninerouter already use.

Why it went unnoticed: this file is only type-checked by `open-sse/tsconfig.json`,
whose runs abort at `TS5101` (the deprecated `baseUrl`) before any file is checked,
and `typecheck:core` covers a curated 26-file allowlist that excludes every executor.
Removing that config error is #8473; this bug is what the first full run surfaced.

TDD: the two new tests fail on the parent commit — `execute()` never reaches the
stubbed `fetch` — and pass with the fix. They also cover the null-signal path, since
that is where an unguarded `mergeAbortSignals` would throw next.
2026-07-25 02:53:13 -03:00
backryun
8eebda13ca refactor(sse): resolve open-sse utils/translator type diagnostics for TS 7 (#8483)
First slice of the TypeScript 7 migration split requested on #7697: resolve the
type diagnostics under `open-sse/tsconfig.json` in the lowest-risk modules, with
no toolchain change. 12 diagnostics across 8 files, all outside the hot path —
`chatCore.ts` and `stream.ts` are deliberately left for a later, standalone slice.

Fixes, by cause:

* `Transformer.cancel` (progressTracker, sseHeartbeat, and stream.ts's existing
  handler) — the WHATWG Streams standard defines `transformer.cancel(reason)` and
  Node implements it (verified on v24: cancelling the readable side invokes it),
  but `lib.dom.d.ts` still omits it from `Transformer`, so every such handler was
  TS2353. These handlers clear the heartbeat/progress intervals when an SSE client
  disconnects, so deleting them to satisfy the checker would leak a timer per
  abandoned stream. The interface is patched in `open-sse/types.d.ts` instead.

* `earlyStreamKeepalive` — `SettledHandler` was discriminated by `ok: true | false`.
  This workspace compiles with `strictNullChecks: false`, where a boolean-literal
  discriminant narrows the positive branch but not the negative one, so reading
  `.error` off the rejected arm did not type-check (the two `.response` reads
  elsewhere in the file did, which is why only one site errored). Retagged with a
  string discriminant, which narrows both branches under the same settings.

* `toolCallShim` / `openai-responses` — assigning back to a property declared
  `unknown` resets the `typeof` narrowing, so the following comparison no longer
  saw a number/array. Both now read through a local. The `Read` limit clamp is
  behavior-identical: its two branches are mutually exclusive at READ_MAX_LIMIT 2000.

* `sanitizeToolResultId` — takes `unknown` but forwards to a `string` parameter; a
  non-string id previously reached `.replace()` and threw. Coerced instead.

* `openaiHelper` — `opts = {}` inferred `{}`; typed as `FilterToOpenAIFormatOptions`.

* `cursorAgentProtobuf` — `Buffer.alloc(0)` infers `Buffer<ArrayBuffer>` under
  @types/node 26 while the decoded field is `Buffer<ArrayBufferLike>`; the locals
  now use bare `Buffer`, matching `requestMetadata` a few lines above.

Validation: 335 -> 321 diagnostics with zero new errors (full tsc error-set diff
against the base config). typecheck:core clean, lint clean, check:type-coverage
92.17% -> 94.17%. All 114 existing test files that import a touched module pass;
`plan3-p0.test.ts` fails identically with and without this change (it reads the
developer's real ~/.omniroute DB instead of a test-scoped DATA_DIR).

The new test covers the three behavioral surfaces rather than the refactors the
existing keepalive/heartbeat suites already hold: that `transformer.cancel()`
really fires and can clear an interval, the id coercion, and the limit-clamp bounds.
2026-07-25 02:53:07 -03:00
backryun
1930b09c6a chore(sse): drop deprecated baseUrl from open-sse tsconfig for TS 7.0 (#8473)
TypeScript 6.x raises TS5101 on `open-sse/tsconfig.json`: `baseUrl` is
deprecated and stops functioning in TypeScript 7.0. It was paired with
`ignoreDeprecations: "5.0"`, which no longer silences it under TS 6 (the
compiler now demands "6.0").

Remove `baseUrl: ".."` and rewrite the `paths` mappings relative to the
tsconfig's own directory, which is how TypeScript resolves them with no
baseUrl set:

  "@/*"                     ./src/*       -> ../src/*
  "@omniroute/open-sse"     ./open-sse    -> ../open-sse
  "@omniroute/open-sse/*"   ./open-sse/*  -> ../open-sse/*

`ignoreDeprecations` goes with it — baseUrl was the only deprecated option
it was suppressing.

Verified by diffing the full tsc error set against the previous config (the
base run used `ignoreDeprecations: "6.0"` so compilation proceeds past the
config error, which otherwise aborts type-checking and masks everything):
zero new errors, 28 fewer. All 28 were in `electron/*.js`, which
`baseUrl: ".."` had been dragging into the open-sse program via
root-relative resolution. Scoping the program back to open-sse also moves
`check:type-coverage` from 92.17% to 94.01%; the ratchet direction is up so
the gate passes, and the baseline is deliberately left alone because the
gain is a measurement-scope change rather than new typing work.

The guard test asserts no tsconfig reintroduces `baseUrl` or
`ignoreDeprecations`, and that every `paths` target still resolves to a real
directory — the second half is the part that matters, since dropping baseUrl
silently changes what those mappings point at.
2026-07-25 02:53:01 -03:00
backryun
909642879f feat: add Claude Opus 5 support (#8464) 2026-07-25 02:52:55 -03:00
Markus Hartung
11f7ba2c72 fix(sse): record tool calls into shared state for openai->openai-responses call-log summary (#8462)
The openai-responses translator tracked tool calls in its own funcCallIds/
funcNames/funcArgsBuf bookkeeping without ever writing to the shared
state.toolCalls Map that stream.ts's completion-log summary builder reads.
Every openai->openai-responses translated stream with a tool call was
persisted with finish_reason "stop" and no tool_calls, even though the
actual SSE events sent to the client were correct.
2026-07-25 02:52:49 -03:00
dependabot[bot]
94125e09b1 chore(deps): bump actions/setup-python from 6 to 7 (#8456)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6 to 7.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-25 02:52:37 -03:00
dependabot[bot]
fb6b8e5233 chore(deps): bump github/codeql-action/init from 4.37.1 to 4.37.3 (#8455)
Bumps [github/codeql-action/init](https://github.com/github/codeql-action) from 4.37.1 to 4.37.3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](7188fc3636...e4fba868fa)

---
updated-dependencies:
- dependency-name: github/codeql-action/init
  dependency-version: 4.37.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-25 02:52:30 -03:00
dependabot[bot]
2a02d63676 chore(deps): bump ossf/scorecard-action from 2.4.3 to 2.4.4 (#8454)
Bumps [ossf/scorecard-action](https://github.com/ossf/scorecard-action) from 2.4.3 to 2.4.4.
- [Release notes](https://github.com/ossf/scorecard-action/releases)
- [Changelog](https://github.com/ossf/scorecard-action/blob/main/RELEASE.md)
- [Commits](https://github.com/ossf/scorecard-action/compare/v2.4.3...v2.4.4)

---
updated-dependencies:
- dependency-name: ossf/scorecard-action
  dependency-version: 2.4.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-25 02:52:24 -03:00
dependabot[bot]
2129aa8a4a chore(deps): bump github/codeql-action/analyze from 4.37.1 to 4.37.3 (#8453)
Bumps [github/codeql-action/analyze](https://github.com/github/codeql-action) from 4.37.1 to 4.37.3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](7188fc3636...e4fba868fa)

---
updated-dependencies:
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.37.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-25 02:52:17 -03:00
dependabot[bot]
592defd488 chore(deps): bump codecov/codecov-action from 5.5.5 to 7.0.0 (#8452)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.5.5 to 7.0.0.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](0fb7174895...fb8b3582c8)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-25 02:52:10 -03:00
Honey Tyagi
0d92be2117 test(e2e): contract test for the full provider journey (#8330) (#8444)
Add an end-to-end contract test that walks the whole provider journey as one gate: create provider (node) -> add connection -> sync models -> select in Combo -> Playground -> /v1/models exposure -> call via API key -> visible in Topology.

Every step asserts against the same derived contract identity (published model id / configured prefix / raw node id), so a divergence on any surface fails the suite. Directly guards the bugs tracked from Discussion #8273: compatible-provider model regex drift, /v1/models namespace/UUID incoherence (#8327), and Topology blind to custom providers (#8328/#3198).

The primary journey drives the real App Router route handlers + DB layer in-process against an isolated DATA_DIR, so it runs in CI under test:integration (collected by the top-level tests/integration/*.test.ts glob) as a blocking gate with no live server. A second, opt-in block runs the same journey over HTTP and self-skips unless RUN_CONTRACT_INT=1 (same convention as the RUN_SERVICES_INT suites).

Refs #8273. Reported-by: @nguyenha935
2026-07-25 02:52:03 -03:00
Prudhvi Vuda
88180d069a feat(providers): add missing opencode-go reasoning effort variants (#8441)
Register OpenCode Go registry effort aliases and EFFORT_TIERS rewrites so
clients can select declared reasoning levels through OmniRoute.

Closes #8353
2026-07-25 02:51:56 -03:00
MumuTW
7cc922ba97 fix(dashboard): preserve connection health visual on last routed topology node (#8428) 2026-07-25 02:51:49 -03:00
MumuTW
3432579eb0 fix(oauth): add devin-cli and agy entries to OAUTH_TEST_CONFIG (#8427) 2026-07-25 02:51:42 -03:00
MumuTW
4528fc455e fix(auth): exclude local CLI providers from tokenHealthCheck expiration (Fixes #8407) (#8426)
* fix(providers): prevent health sweep from expiring devin-cli local credentials

* fix(auth): drop devin-cli from supportsTokenRefresh explicit set (#8407)

Root cause: listing "devin-cli" as refresh-capable made tokenHealthCheck
force-expire local CLI connections that never have a refresh token.

Remove it from the explicit set (keep windsurf) and drop the health-check
provider hardcode — the existing supportsTokenRefresh=false guard is enough.
2026-07-25 02:51:35 -03:00
MumuTW
73c5e27379 i18n(zh-TW): translate missing Reasoning Routing strings (#8423) 2026-07-25 02:51:28 -03:00
Dvir Arad
49ccc73f44 docs(claude-code): document unprefixed model IDs and the Ambiguous model error (#8410)
Claude Code always sends bare (unprefixed) model IDs such as
claude-opus-4-8. When both the Claude Code (cc) and Claude (claude)
providers are connected, that bare id resolves to two routes and the
gateway returns a 400 'Ambiguous model' error, which the guide never
mentioned.

Add a Troubleshooting entry covering both fixes: pinning a prefixed
ANTHROPIC_MODEL, or enabling the 'Prefer Claude Code for unprefixed
Claude models' setting (dashboard toggle /
OMNIROUTE_PREFER_CLAUDE_CODE_FOR_UNPREFIXED_CLAUDE_MODELS), linking to
the environment reference where the flag is documented.

Closes #8311
2026-07-25 02:51:22 -03:00
Diego Rodrigues de Sa e Souza
5d9ace6778 feat(providers): map upstream reasoning-level metadata in openai-compatible discovery (#8347) (#8363) 2026-07-25 02:51:15 -03:00
ikelvingo
9dcbbd18c8 fix(i18n): restore brand proper nouns and unify terminology in zh-CN and zh-TW (#8355)
* chore(ci): add .mergify.yml to main — Mergify only reads config from the default branch (#7168)

* fix(ci): add the auto-enqueue pull_request_rule to the Mergify config (queue_conditions alone are eligibility-only) (#7179)

* fix(ci): migrate Mergify auto-enqueue to merge_protections_settings.auto_merge_conditions (rules-based path is EOL 2026-07-16) (#7216)

* fix(ci): drop Mergify batch settings (batching is a paid-tier feature; free plan queue is serial) (#7220)

* fix(ci): merge queue tolerates the advisory dast-smoke failure (its GH-hosted build hang dequeued every attempt) (#7225)

* test(ci): make the #6634 selfref guard hermetic — main's copy hard-fails every PR (#7341)

main's copy of this test still does git I/O inside a unit test:

    const baseSrc = git(['show', 'origin/main:' + FILE]);

Runners check out a shallow single ref, so origin/main does not resolve and the
test dies with 'fatal: invalid object name origin/main'. Every PR into main
fails Unit Tests (7/8) on it — today that is #7313, #7315, #7316, #7334, #7336
and #7337, six PRs red on a defect none of them introduced. #7313 has no other
red at all.

release/v3.8.49 already carries a fix (2e42b8efc, #7174: try/catch, fetch
origin/main on demand, t.skip() when unreachable), but it only reaches main at
release time — so main stays broken for the whole cycle. Cherry-picking it would
also import a new problem: PR Test Policy classifies t.skip() as a silenced
assertion, which we watched it correctly catch on #7300 today.

This is the hermetic version instead (ported from #7327, which does the same for
the release branch): read the file straight off disk, compare against an empty
base so baseTaut/baseExtTaut are 0 — the strictest possible comparison point —
and call evaluateMasking() directly. No git ref, no fetch, no skip, nothing the
runner's checkout depth can break.

The #6634 regression stays covered: the guard's logic lives in
SELF_TEST_FIXTURE_RE (check-test-masking.mjs:337), not in the test. Proven both
ways on main before committing — neutralise SELF_TEST_FIXTURE_RE to /$^/ and
the test FAILS; restore it and it passes 2/2, with check-test-masking.mjs left
byte-identical.

Co-authored-by: growab <nekron@icloud.com>

* chore(quality): tighten main's coverage baseline to the CI's real numbers (#7347)

main's ratchet had been failing --require-tighten on every PR: 11 metrics
improved but the baseline was never tightened. Same class as the #6634
selfref guard — an infra fix that lands only on the release branch leaves
main red for the whole cycle, and every PR into main pays for it.

Values are the merged-coverage numbers from a run on main itself (a local
run measures ~68% vs CI's ~80%; the baseline's own note warns about that
gap). Only the 11 coverage values change — gitleaks and semgrepFindings
keep main's own state.

No changelog fragment: #7326 carries it on release/v3.8.49, and a second
one here would double the entry at release time.

* Add cliproxy provider exposure controls and manifest injection (#7329)

* feat(fusion): let judge use its own knowledge and override the panel (#6804)

The judge prompt said to write an answer 'grounded in that analysis',
implicitly capping output at the panel's union. When all panel members
miss or are collectively wrong on something, the judge should apply its
own reasoning as a full participant and override consensus, while keeping
an honesty guard against fabrication. Adds a regression test.

Co-authored-by: Chirag Singhal <chirag127@users.noreply.github.com>

* fix(api): raise provider apiKey cap for cookie-based web providers (#6715) (#6759)

* fix(cli): fall back to settings.json when Claude Code binary is unresolvable (#6701) (#6734)

getCliRuntimeStatus() only ever answered `installed` from binary resolution
(known install paths + where/which PATH search), so a stale PATH, moved
binary, or uncatalogued install method reported "not found" even when
~/.claude/settings.json proved the CLI was installed and used before —
regressing behind upstream 9router's checkClaudeInstalled(), which already
falls back to the settings file when where/which fails.

withSettingsFallback() (new src/shared/services/cliInstallFallback.ts, kept
out of the frozen cliRuntime.ts to respect its file-size ceiling) restores
that parity: only when the binary lookup's own reason is "not_found" (never
for deliberate security rejections like unsafe/relative env overrides or
symlink escapes) and the tool's settings file exists on disk.

* fix(providers): honor explicit thinking.budget_tokens 0 in openai->gemini transform (#6813) (#6821)

The transform forwarded the Claude-style thinking.budget_tokens into
generationConfig.thinkingConfig.thinkingBudget, but the presence check was
truthy (&& thinking.budget_tokens). An explicit budget_tokens: 0 — the
natural way to disable thinking — is falsy, so it was dropped and the
request fell through to the default thinkingConfig injection, making the
model think despite an explicit request for zero. Use an explicit numeric
check so 0 is honored as thinkingBudget 0; includeThoughts is only set for
a non-zero budget.

* fix(compression): reconcile outer vs per-engine token counts (#6488) (#6741)

* fix(compression): reconcile outer vs per-engine token counts on degenerate output (#6488)

Outer originalTokens/compressedTokens (real tiktoken counter over extracted
message text) diverged from engineBreakdown[0]'s counts (a crude
JSON.stringify(requestBody).length/4 estimate), worst on small/degenerate
inputs where JSON structural overhead dominates. A single-engine breakdown
entry represents the exact same before/after transformation as the overall
response, so reconcileSingleEngineTokens() now overwrites that one entry's
counts with the outer, more accurate figures; multi-step pipeline
breakdowns are left untouched.

* chore(6741): resolve release sync — CHANGELOG.md restored to release tip, entry moved to changelog.d fragment (fragments-first)

* fix(api): accept enableRenderers in RTK compression config schema (#6703) (#6757)

* fix(db): break probe-failed/restore loop on large storage.sqlite (#6632)

Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR); keeps only the author's changes.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* feat(cursor): add Opus 4.8, Fable 5, and Sonnet 5 model families (#6779)

Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR); keeps only the author's cursor registry + test changes.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(translator): read PDF/video file attachments for Gemini/Antigravity and Claude (#6790)

Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR); keeps only the author's translator + test changes.

Co-authored-by: Wital <witalorocha216@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(codex): strip include from compact responses requests (#6805)

* fix(codex): strip include from compact responses requests

Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR); keeps only the author's changes.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(6805): move include-strip assertion to standalone test file to keep executor-codex.test.ts under frozen size cap

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6769)

Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR); keeps only the author's changes.

Co-authored-by: Chirag Singhal <chirag127@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(bootstrap): filter empty process.env values to prevent Docker env crash loop (#6828)

Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR) and the direct CHANGELOG.md edit (fragments-first);
keeps only the author's bootstrap change.

Co-authored-by: Andrian B. <andrewbalanesq@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(providers): update SenseNova Token Plan support (#6330)

Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR); the author's constants/registry/snapshot deltas
were re-applied cleanly onto the release tip.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(providers): classify 404 as MODEL_NOT_FOUND to stop retry storm (#6829)

Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR) and the direct CHANGELOG.md edit (fragments-first);
the author's chatCore/errorClassifier deltas were re-applied cleanly onto the release tip.

Co-authored-by: Andrian B. <andrewbalanesq@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(api): accept all catalog engines on compression PUT schema (#6792)

Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR). Resolved the release's OmniGlyph engine addition
additively (types.ts/compression.ts kept both 'relevance' and 'omniglyph') and extended
stackedPipelineStepSchema + STACKED_PIPELINE_ENGINE_INTENSITIES with the omniglyph branch
so the ENGINE_CATALOG-parity test passes.

Co-authored-by: Pitchfork-and-Torch <Pitchfork-and-Torch@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(api): point CLI health command at /api/monitoring/health (#6677) (#6717)

* fix(api): point CLI health command at /api/monitoring/health (#6677)

bin/cli/commands/health.mjs called GET /api/health, a route that was
moved to /api/monitoring/health without updating the CLI; the top-level
/api/health handler never existed on disk (only degradation/ and ping/
sub-routes). Point runHealthCommand()/runHealthComponentsCommand() at
/api/monitoring/health and read its real payload shape
(activeConnections, circuitBreakers: {open,halfOpen,closed}, memoryUsage)
instead of the old nonexistent requests/breakers/cache/memory fields.

* chore(6717): re-sync onto release tip; move CHANGELOG entry to changelog.d fragment (fragments-first)

* chore(cursor): add Grok 4.5 effort/fast model IDs (#6774)

Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR); keeps only the author's changes.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED (#6791)

* fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED

Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR); keeps only the author's changes.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* refactor(deepseek): extract done-terminator helper to keep frozen file under cap

Extracts the FINISHED-drain scheduler and finish-once guard added for
the [DONE] terminator fix (#6777) into a new
deepseek-web-done-terminator.ts module, so deepseek-web.ts stays under
its frozen line cap (1148). Behavior is unchanged.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Pitchfork-and-Torch <Pitchfork-and-Torch@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* feat(models): add capability override UI (#6727)

* feat(models): add capability override UI

Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR); renumbered the migration 118 -> 119 to resolve the
collision with 118_provider_param_filters.sql already on release/v3.8.47; the author's
i18n/localDb deltas were re-applied cleanly onto the release tip.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(6727): import model-capability-overrides DB fns directly (not via localDb barrel) to keep localDb under file-size cap; aligns with anti-barrel convention

* chore(db): satisfy known-symbols contract for modelCapabilityOverrides

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>

* fix(cursor): use Agent CLI build id for x-cursor-client-version (#6795)

* fix(cursor): use Agent CLI build id for x-cursor-client-version

Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR); the author's .env.example/docs deltas were
re-applied cleanly onto the release tip.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* chore(changelog): re-sync CHANGELOG.md to release tip (restore #6701 bullet)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(startup): rename reasoningControls.ts to avoid webpack casing collision (#6584) (#6718)

* fix(startup): rename reasoningControls.ts to avoid webpack casing collision (#6584)

* chore(6718): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first)

* fix(build): suppress Turbopack over-bundling warning from agentSkills generator (#6582) (#6720)

* fix(build): suppress Turbopack over-bundling warning from agentSkills generator (#6582)

generator.ts builds outputBase from a non-literal outputDir parameter, so
Turbopack's file-tracing analyzer can't narrow it and emits an "Overly broad
patterns" warning per entry point that imports the module (603 warnings on
v3.8.46, up from 379). The fs access is legitimate and bounded, so
next.config.mjs now suppresses this specific diagnostic via turbopack.ignoreIssue,
mirroring the existing webpack.ignoreWarnings precedent in the same file.

* chore(6720): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first)

* fix(providers): drop image_generation for Codex Spark models regardless of plan (#6651) (#6721)

* fix(providers): drop image_generation for Codex Spark models regardless of plan (#6651)

* chore(6721): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first)

* fix(providers): stop quota card re-sorting Codex/GLM bars by remaining % (#6687) (#6722)

* fix(providers): stop quota card re-sorting Codex/GLM bars by remaining % (#6687)

QuotaCardExpanded.tsx unconditionally re-sorted quotas by remaining
percentage via sortQuotasByRemaining(), discarding the deterministic
CODEX_QUOTA_ORDER/GLM_QUOTA_ORDER window order quotaParsing.ts's
sortCodexOrder()/sortGlmOrder() had already established. A new
hasFixedQuotaOrder() + resolveQuotaDisplayOrder() skip the re-sort for
providers with a fixed window order (codex, glm family), threading
providerId from QuotaCard.tsx through to the display layer.

Regression guard: tests/unit/quota-card-expanded-fixed-order-6687.test.ts

* chore(6722): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first)

* fix(startup): lazy-import ioredis in rateLimiter to fix MCP ERR_MODULE_NOT_FOUND (#6559) (#6725)

* fix(startup): lazy-import ioredis in rateLimiter to fix MCP ERR_MODULE_NOT_FOUND (#6559)

* chore(6725): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first)

* fix(resilience): resolve fp-pinned combo account back to real connection id (#6696) (#6732)

* fix(resilience): resolve fp-pinned combo account back to real connection id (#6696)

* chore(6732): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first)

* fix(api): Responses passthrough emits event-only SSE frames after filtering commentary output (#6561) (#6735)

* fix(api): Responses passthrough emits event-only SSE frames after filtering commentary output (#6561)

The #6199 commentary-drop `continue;` branches in stream.ts skipped the
data: line for a dropped commentary event but never cleared the
already-buffered event: line for the same frame, so the next blank line
flushed the stale event: line alone -- an event-only SSE frame that
crashes the OpenAI Python SDK's json.loads(). Both drop sites now call
clearPendingPassthroughEvent() before continue. The commentary-drop
decision was extracted into a new responsesCommentaryDrop.ts module so
the fix does not grow the frozen stream.ts.

* chore(6735): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first)

* fix(api): emit reasoning_content on claude-web + v0-vercel-web SSE (#6662) (#6743)

* fix(api): emit reasoning_content on claude-web + v0-vercel-web /v1/chat/completions SSE (#6662)

* chore(6743): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first)

* fix(sse): unwrap bare {function:{…}} tools in openai→claude translation (#6704)

* fix(sse): unwrap bare {function:{…}} tools in openai→claude translation

Some OpenAI-shape clients send a tool as a bare `{ function: {...} }`
object, omitting the spec-required `type: "function"` parent wrapper.
The tools-mapping in openai-to-claude.ts (~line 366) only unwrapped
`tool.function` when `tool.type === "function"` was ALSO true, so a
bare-function tool fell through to `toolData = tool` (the wrapper
itself, with no `.name`), producing an empty `originalName` and
silently dropping the tool from the translated request — worse than
a 400, since the caller has no signal the tool never made it
upstream. Unwrap `tool.function` whenever present, independent of
the parent `type` field. Regression guard:
tests/unit/openai-to-claude-bare-tool.test.ts.

Co-authored-by: Samir Abis <me@samirabis.com>
Inspired-by: https://github.com/decolua/9router/pull/2473

* chore(6704): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first)

---------

Co-authored-by: Samir Abis <me@samirabis.com>

* fix(oauth): avoid bare-email dedup of Codex OAuth logins (#6706)

* fix(oauth): avoid bare-email dedup of Codex OAuth logins

When an incoming Codex OAuth connection has no verifiable workspace/account
id, do not merge it into an existing row on email match alone — that
silently overwrote the other account's token pair. Require a matching
chatgptUserId (a stable per-account JWT id) before merging; otherwise
insert a distinct connection row.

Co-authored-by: lucasjustinudin <34107354+lucasjustinudin@users.noreply.github.com>
Inspired-by: https://github.com/decolua/9router/pull/2477

* chore(6706): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first)

---------

Co-authored-by: lucasjustinudin <34107354+lucasjustinudin@users.noreply.github.com>

* fix(sse): skip thinkingConfig for gemma models in openai→gemini translation (#6708)

open-sse/translator/request/claude-to-gemini.ts already guards against
sending thinkingConfig for gemma-4-* models (Gemma doesn't support it —
Vertex returns 400: "Thinking budget is not supported for this model"),
but the OpenAI-shape path (openai-to-gemini.ts) lacked the same guard, so
OpenAI-shape clients hitting a vertex gemma-4-* model still got a 400.

Mirrors the existing claude-to-gemini.ts guard: wrap the reasoning_effort
and Claude-shape thinking.budget_tokens branches with a model.startsWith
("gemma-4") check. Branch 3 (default includeThoughts for modern Gemini
models) already excludes non-"gemini" model ids and needed no change.


Inspired-by: https://github.com/decolua/9router/pull/2480

Co-authored-by: chy1211 <31048289+chy1211@users.noreply.github.com>

* fix(codex): surface capacity errors embedded in 200-OK SSE streams (#6710)

* fix(codex): surface capacity errors embedded in 200-OK SSE streams

Codex sometimes answers with HTTP 200 and a text/event-stream body whose
payload carries a transient error mid-stream (e.g. "Selected model is at
capacity...", server_is_overloaded, service_unavailable_error). Because the
outer HTTP status was 200, this looked like a successful response to every
caller — no retry, no circuit breaker, and no combo/account fallback ever
engaged, so a healthy account sat idle while the request silently failed or
truncated.

Add peekCodexSseTransientError() to open-sse/executors/codex.ts: it peeks the
first bytes of a text/event-stream Codex response, pattern-matches the known
transient-error signatures, and converts a match into a real 503 Response via
errorResponse() (Hard Rule #12 — sanitized, never raw upstream text). A 503 is
already a recognized provider-failure status in accountFallback.ts, so combo
routing and connection cooldown pick it up automatically. When no error
signature is found, the peeked prefix is prepended back onto the remaining
upstream body so the passthrough stays byte-identical to the unmodified
response.

Regression guard: tests/unit/codex-sse-capacity-fallback.test.ts — a
model-at-capacity payload and a server_is_overloaded/service_unavailable_error
payload both convert to 503; a normal single-chunk SSE stream and one split
across multiple network chunks both reassemble byte-for-byte unchanged.

Inspired-by: https://github.com/decolua/9router/pull/2452 (sub-bug #3 only —
OmniRoute already covers PR #2452's other two sub-bugs: service_tier "fast"
normalization and reasoning_effort "max" normalization).

Co-authored-by: ryanngit <74137224+ryanngit@users.noreply.github.com>

* chore(6710): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first)

---------

Co-authored-by: ryanngit <74137224+ryanngit@users.noreply.github.com>

* fix(volcengine): clamp Kimi max_tokens to Ark endpoint cap (#6712)

* fix(volcengine): clamp Kimi max_tokens to Ark endpoint cap

VolcEngine Ark's Kimi coding-plan endpoint (ark.cn-beijing.volces.com)
enforces max_tokens <= 32768 server-side and returns 400 "integer above
maximum value, expected a value <= 32768" for anything over that ceiling.
OmniRoute's StripRule only supported dropping params outright, with no
numeric clamp mechanism, so a client sending a larger max_tokens (common
default, e.g. 65536) 400s outright against volcengine's kimi-k2-5-260127.

The 32768 cap is independently confirmed against two live-endpoint bug
reports hitting this exact Ark endpoint for both kimi-k2.5 and
kimi-k2.7-code (NousResearch/hermes-agent#51773, MoonshotAI/kimi-cli#1124),
not just upstream's own value — same cap upstream 9router#2460 uses.

StripRule gains two optional fields: `clampToModelMaxOutput` (clamp to the
model's own catalog maxOutputTokens ceiling, when set) and `maxOutputCap`
(a fixed endpoint-imposed ceiling); when both apply, the lower wins. The
new rule is scoped to the literal id `kimi-k2-5-260127` (OmniRoute's real
volcengine Kimi model, not upstream's `Kimi-K2.7-Code`), not a broad
/kimi/i regex, so it can never clamp an unrelated future Kimi listing
whose Ark cap may differ. glm-4-7-251222 (the other volcengine model) is
unaffected.

Inspired-by: https://github.com/decolua/9router/pull/2460
Co-authored-by: whale9820 <whale9820@users.noreply.github.com>

* chore(6712): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first)

---------

Co-authored-by: whale9820 <whale9820@users.noreply.github.com>

* fix(antigravity): surface aborted Gemini tool calls off end_turn (#6713)

* fix(antigravity): surface aborted Gemini tool calls off end_turn

Gemini/Antigravity aborts a turn with finishReason MALFORMED_FUNCTION_CALL
(or a sibling like UNEXPECTED_TOOL_CALL) instead of completing cleanly. Both
Claude-facing translators collapsed these to a clean end_turn, hiding the
aborted tool call as a successful completion:
- the OpenAI hub path (openai-to-claude.ts convertFinishReason default), and
- the DIRECT Gemini->Claude path (gemini-to-claude.ts), which is the one
  Claude Code actually hits through an antigravity/Gemini-routed model.
Add isAbortFinishReason() to finishReason.ts and map these reasons to
tool_use on both paths; genuinely unknown reasons still fall back to end_turn.

Co-authored-by: anhdiepmmk <n08ni.dieppn@gmail.com>
Inspired-by: https://github.com/decolua/9router/pull/2462

* chore(6713): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first)

---------

Co-authored-by: anhdiepmmk <n08ni.dieppn@gmail.com>

* fix(translator): strip empty cloud_base_branch from Cursor Subagent tool call (#6729)

* fix(translator): strip empty cloud_base_branch from Cursor Subagent tool call (port from 9router#2446)

The Responses->Chat tool-arg cleanup (stripEmptyOptionalToolArgs) only stripped
empty-string/empty-array optional args for Claude Code's Read tool. Cursor's local
Subagent tool call therefore passed through with the cloud-only field
cloud_base_branch: "", which Cursor rejects ("cloud_base_branch may only be specified
when environment equals cloud") before starting the subagent. Extend the cleanup to an
allowlist of Read + Subagent; arbitrary tools stay untouched.

Reported-by: like3213934360-lab (https://github.com/decolua/9router/issues/2446)

* chore(6729): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first)

* fix(translator): defer content_block_start until GLM streams the tool name (#6730)

* fix(translator): defer content_block_start until GLM streams the tool name (port from 9router#2077)

GLM 5.2 (and similar OpenAI-compatible upstreams) stream a tool call's id and
function.name across separate SSE delta chunks. The openai-to-claude streaming
translator emitted content_block_start immediately on the id-only chunk with an empty
name; the Claude SSE protocol cannot patch a block after emission, so the later
name-only chunk was dropped and Claude Code rejected the tool_use with an empty tool
name / "No such tool available:". Defer content_block_start until the name arrives
(start on args if they arrive first), and emit a start for any orphaned id-only tool
call at finish so content_block_stop is never orphaned.

Reported-by: itiwant (https://github.com/decolua/9router/issues/2077)

* chore(6730): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first)

* feat(dashboard): add search to Playground model picker dropdown (#4086) (#6811)

* feat(dashboard): add search to Playground model picker dropdown (#4086)

The shared ModelSelectModal (combo builder + CLI-code cards) already had
search, but the Playground's raw model <select> in StudioConfigPane stayed
a flat unsearchable list - unusable once a provider like OpenRouter
contributed 50+ models.

Adds a search input above the dropdown that filters options via
filterModelsByQuery() (Turkish-safe accent/case-insensitive match, reusing
matchesSearch()). The currently selected model always stays pinned in the
list even when it doesn't match the query, so typing never silently swaps
the active selection. Reuses the existing common.search i18n key already
translated in all 42 locales - no new key needed.

* chore(6811): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first)

* feat: request count log per provider, per date (#4009) (#6812)

* feat(dashboard): request count log per provider, per date (#4009)

Some providers bill by request rather than by token, so operators need
a plain per-provider, per-date request count breakdown, not just token
aggregates. Adds a new getProviderDailyUsageRows() aggregation query
(src/lib/db/usageAnalytics.ts), a dedicated GET
/api/usage/requests-by-provider-date route (kept separate from the
frozen /api/usage/analytics route to respect the file-size baseline),
and a sortable, single-date-filterable table on Dashboard -> Analytics.

Closes #4009

* chore(6812): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first)

* feat(xai): route xAI clients to Grok native /v1/responses endpoint (#6709)

* feat(xai): route xAI clients to Grok native /v1/responses endpoint

xAI ships a native /v1/responses endpoint (https://api.x.ai/v1/responses)
alongside /v1/chat/completions, but XaiExecutor extended BaseExecutor
without overriding buildUrl(), so every request always resolved to the
static chat-completions baseUrl regardless of target format — the last
genuinely-missing slice of decolua/9router#2439 (grok-build-0.1, the
reasoning-effort suffix routing, and bare grok-* routing were already
ported in prior cycles).

Add responsesBaseUrl to the xai registry entry and tag
grok-4.20-multi-agent-0309 (upstream's own Responses-only id) with
targetFormat: "openai-responses", mirroring the existing model-tag-driven
routing pattern already used by the gh executor (9router#102) and the
"openai" -pro heuristic in open-sse/executors/default.ts — the per-model
registry tag is the single source of truth that also drives chatCore's
body translation, so URL and body stay in lockstep. XaiExecutor.buildUrl
now checks getModelTargetFormat("xai", model) and resolves to the native
Responses endpoint only for tagged models, leaving every other grok-*
model on the existing chat-completions bridge.

TDD: tests/unit/executor-xai.test.ts adds a RED-then-GREEN case asserting
grok-4.20-multi-agent-0309 resolves to https://api.x.ai/v1/responses and
a control case asserting grok-4.3 still resolves to
https://api.x.ai/v1/chat/completions.

Co-authored-by: ryanngit <74137224+ryanngit@users.noreply.github.com>
Inspired-by: https://github.com/decolua/9router/pull/2439

* chore(6709): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first)

---------

Co-authored-by: ryanngit <74137224+ryanngit@users.noreply.github.com>

* fix(resilience): route remaining credential-selection call sites through quota preflight (#6686) (#6742)

* fix(resilience): route remaining credential-selection call sites through quota preflight (#6686)

* chore(6742): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first)

* fix(resilience): apikey-provider 429s honor explicit quota-exhausted text (#6638) (#6731)

* fix(resilience): apikey-provider 429s honor explicit quota-exhausted text (#6638)

Ollama Cloud (and any other apikey-category provider) 429s skipped body-text
quota classification entirely; a genuine multi-day quota exhaustion was
misclassified as a plain rate_limit_exceeded with a few seconds of cooldown,
so combo routing retried the account immediately. shouldPreserveQuotaSignals()
now lets an explicit quota-exhausted signal (looksLikeQuotaExhausted) override
the apikey-category default, and parseDayGranularityResetMs() adds day-
granularity reset-hint parsing ("...reset in 3 days.") alongside the existing
Xh/Ym/Zs parsing.

Regression guard: tests/unit/issue-6638-ollama-quota.test.ts (RED before the
fix, GREEN after). Aligned two tests/unit/account-fallback-service.test.ts
cases that had codified the old buggy behavior for apikey-provider quota
text.

* chore(6731): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first)

* feat(resilience): weekly-429 cooldown for fetcher-less providers (#3709) (#6817)

* feat(resilience): weekly-429 cooldown for fetcher-less providers (#3709)

Ollama Cloud free-tier accounts have a hard WEEKLY request cap. On cap the
upstream returns 429 "you (<account>) have reached your weekly usage
limit", but ollama-cloud is an apikey-category provider, so the existing
oauth-only shouldUseQuotaSignal gate in checkFallbackError skips the
subscription-quota-text classifier (Issue #2321) for its 429s -- the
account fell through to the generic exponential backoff (~1s, capped at
2min) and got retried every few minutes for the rest of the week (one
account took 285x429 in 48h).

Adds a new, ungated weekly-usage-limit text classifier that applies a 24h
QUOTA_EXHAUSTED cooldown regardless of provider category. Extracted the new
classifier -- together with the existing #2321 subscription-quota logic --
into a new open-sse/services/quotaTextCooldowns.ts module so the frozen
accountFallback.ts (file-size-baseline cap) didn't have to grow; net effect
shrinks accountFallback.ts by 20 lines.

This is Phase A of the plan (open-sse/services/accountFallback.ts:1038-1045
"weekly-429 cooldown"); Phase B (generic local request-counter preflight for
manual provider_plans dimensions) is a separate, larger follow-up per the
plan's own phasing.

* chore(6817): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first)

* fix(providers): Kiro adaptive-thinking allowlist excludes sonnet-4.5/haiku-4.5 (#6576) (#6726)

* fix(providers): Kiro adaptive-thinking allowlist excludes sonnet-4.5/haiku-4.5 (#6576)

* chore(6726): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first)

* test(kiro): migrate selector-strip test to claude-sonnet-5 (only Kiro adaptive-thinking model, #6576)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* chore(quality): rebaseline complexity 2053->2054 (merge-burst drift, v3.8.47)

Inherited drift from today's /implement-prs merge burst (~36 PRs). check:complexity
does not run on the PR->release fast-path, so the branch accrued +1 unmeasured. No
orphan/feature PR introduces a NEW violation (complexity-net-zero); the only flagged
function is the pre-existing getResolvedModelCapabilities. Owner-approved rebaseline
to unblock the FQG of ~7 green-except-complexity orphans.

* chore(stryker): register ollama-quota covering tests (merge-burst drift, v3.8.47)

The 3 covering unit tests from #6731/#6817/#6742 (issue-6638-ollama-quota,
ollama-cloud-weekly-quota-cooldown-3709, issue-6686-quota-preflight-coverage)
exist on release but were never added to tap.testFiles when those PRs merged.
Completes the registration so mutant kills count; unblocks every PR touching a
mutated module. Part of the owner-approved merge-burst drift cleanup.

* fix: auto-start WS server in-process and change default port to 20132 (#6072)

* feat: change default LIVE_WS_PORT from 20129 to 20132

Update the default WebSocket port for the live dashboard server from 20129 to 20132 across all configuration files, documentation, code comments, and tests. Also consolidate OMNIROUTE_DISABLE_LIVE_WS and OMNIROUTE_ENABLE_LIVE_WS into a single OMNIROUTE_ENABLE_LIVE_WS flag. Wire the live WebSocket server to start in-process via instrumentation-node.ts.

* feat: clarify NEXT_PUBLIC_LIVE_WS_PUBLIC_URL path usage and derive upgrade path from URL

Update .env.example and ENVIRONMENT.md to document that the pathname portion of NEXT_PUBLIC_LIVE_WS_PUBLIC_URL (e.g. /live-ws) is used as the WebSocket upgrade path by the dev proxy, handshake response, and client connection logic.

Extract deriveLiveWsPath() into shared/utils/wsPath.ts and wire it through:
- src/app/api/v1/ws/route.ts — handshake response path field
- src/hooks/useLiveDashboard.ts — build

* fix: use the standard URL API to safely parse and update the effectiveWsUrl

* build(docker): expose live WebSocket server port and configure CORS origins

Add LIVE_WS_PORT (20132), LIVE_WS_HOST (0.0.0.0), and LIVE_WS_ALLOWED_ORIGINS environment variables to all Docker Compose profiles and expose the WebSocket port mapping. Prevent infinite self-loop in standalone-server-ws.mjs by skipping proxy when the server itself is running on the LiveWS port.

* docs(env): fix comment formatting for HOST and HOSTNAME variables

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(logs): prevent stale detail refresh reopening modal (#6323)

* fix(logs): prevent stale detail refresh reopening modal

* chore(stryker): register ollama-quota covering tests (release drift from merge burst)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* \ feat: operator-configurable account rotation\ (#6763)

* feat(resilience): operator-configurable account rotation

Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR); the author's accountFallback/.env deltas were
re-applied cleanly onto the release tip.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* docs(env): document configurable account-rotation env vars in ENVIRONMENT.md

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* refactor(rotation): extract rotation gate/context helpers to keep accountFallback.ts under frozen cap

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* chore(changelog): re-sync CHANGELOG.md to release tip (restore lost base bullet)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* test(stryker): register rotation-config test in tap.testFiles for mutation coverage

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* test(stryker): register ollama-quota covering tests (drift from #6731/#6817/#6742) + re-sync

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>

* fix(lmarena): modernize Arena web provider + static Direct-chat catalog (#6280)

* fix(lmarena): modernize Arena web provider + static Direct-chat catalog

Update the lmarena provider for arena.ai (product rebranded from LMArena):

- Route chat via arena.ai create-evaluation with Chrome TLS impersonation
  (tls-client-node) and optional browser-minted recaptchaV3Token.
- Seed Text+Search (48) into the chat registry; seed Image (27) only into
  IMAGE_PROVIDERS. Disable live HTML model discovery; resolve public names to
  Arena UUIDs from the static TypeScript allowlist (no scrape JSON in-repo).
- Soft-exclude 404/502 model ids; slow/stop bulk test-all probes for this provider.
- Do not fold IMAGE_PROVIDERS/video specialty into the chat provider catalog when
  a chat registry already exists (lmarena/openai/xai).
- Display name Arena (Free); keep wire id `lmarena` / alias `lma` for back-compat.
- Theme-aware provider icons: arena-light.svg / arena-dark.svg.
- Preserve split Supabase SSR cookie reconstruction for arena-auth-prod-v1.*.

* fix(providers): align provider-models-route test fixture + regen provider reference

Fold the topaz image-only catalog entry's apiFormat/supportedEndpoints
into the local-catalog test fixture (route now tags media-only
providers per the lmarena PR's staticModels.ts change), regenerate
PROVIDER_REFERENCE.md against the merged release providers.ts, and add
the changelog fragment for #6280.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* test: align web-cookie fallback suite — lmarena now has a registry entry (probe path)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* docs(changelog): reconcile 3-day merge burst — 16 fragments, 4 promised credits, contributors hall 32→63

- changelog.d fragments for the 20 merged PRs that landed without a bullet
  (#6072 #6308 #6323 #6538 #6556 #6586 #6611 #6647 #6675 #6698 #6757 #6759
  #6804 #6821 + ci rollup #6781/#6691/#6693 + docs rollup #6643/#6644/#6646/#6663;
  omniglyph bump #6661 folded into the #6556 bullet)
- deliver the 4 credits promised in close comments but never written:
  @alltomatos (#6819 dup of #6721), @samimozcan (#6762/#6753 subsumed by #6790),
  @chirag127 (#6756 dup of #6757), @Squawk7777 (#6565 dup of #6564 — appended to
  the existing #6564 bullet; changelog-integrity flags that edit as a removal,
  intentional: ALLOW_CHANGELOG_REMOVALS justification)
- rebuild the v3.8.47 Contributors hall from merged-PR authors + thanks credits
  + prior hall: 32 → 63 contributors

* Clamp reasoning token buffer to model output cap (#6714)

* fix(combo): clamp reasoning buffer to model output cap

* fix(routing): preserve near-cap reasoning max tokens

* fix(routing): getExplicitModelOutputCap falls through to registry cap on non-numeric synced limit_output

getExplicitModelOutputCap short-circuited to null whenever a synced
capability row existed, even if that row's limit_output was not a number
(models.dev commonly omits it). That silently disabled the reasoning-token
buffer clamp for any model with a synced row lacking an output limit.

Now only return the synced value when it IS a number; otherwise fall
through to registryModel.maxOutputTokens / spec.maxOutputTokens, matching
the ??-chain precedence already used by getResolvedModelCapabilities().

Adds a standalone regression test (proves the fallthrough returns the real
registry cap, not null) and hardens the #6274 fixture id so its no-output-cap
case does not prefix-match the real glm-5.2 static spec.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* chore(stryker): register ollama-quota covering tests (release drift from merge burst)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* feat(i18n): add Traditional Chinese (zh-TW) localization for frontend and CLI (#6320)

* feat(i18n): add Traditional Chinese (zh-TW) localization for frontend and CLI

- Add src/i18n/messages/zh-TW.json translating frontend web UI
- Add bin/cli/locales/zh-TW.json translating CLI commands and descriptors
- Register zh-TW in config/i18n.json and docs/guides/I18N.md
- Update scripts/i18n/generate-multilang.mjs matching the new locale setup

* fix: update i18n locale count from 42 to 43 after adding zh-TW

The docs strict checker (check-docs-counts-sync.mjs) validates that
README.md and I18N.md reflect the real locale count. Adding zh-TW
bumped the count from 42 → 43.

* fix(i18n): translate providers free-filter labels in zh-TW (#6694 guard)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: lunkerchen <lunkerchen@users.noreply.github.com>

* feat(proxy): implement latency-optimized proxy rotation strategy (#6798)

* feat(proxy): implement latency-optimized proxy rotation strategy

Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR) and the direct CHANGELOG.md edit (fragments-first);
the author's env/docs/i18n deltas were re-applied cleanly onto the release tip.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(proxy): add latency-rotation env var to .env.example

PROXY_LATENCY_WINDOW_HOURS was referenced in src/lib/db/proxies.ts and
documented in docs/reference/ENVIRONMENT.md, but missing from
.env.example, tripping the env/docs sync gate.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* chore(changelog): re-sync CHANGELOG.md to release tip

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* refactor(proxy): extract latency-strategy helpers to keep frozen files under cap

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* test(db-rules): expect 35 audited modules (proxyLatency joins INTENTIONALLY_INTERNAL)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* docs(readme): fix stale strategy/tool/scoring counts (#6853)

README still claimed 17 routing strategies (the table was missing
pipeline), 95 MCP tools, and 9-factor Auto-Combo scoring. Align with
the source (ROUTING_STRATEGY_VALUES has 18 entries) and the canonical
docs (MCP-SERVER.md: 94 tools; AUTO-COMBO.md: 12-factor).

* fix(antigravity): sanitize Cloud Code safety settings (#6839)

Co-authored-by: kfiramar <83420275+kfiramar@users.noreply.github.com>

* fix(kiro): probe IdC region during profileArn discovery, cross-region (recovers #6099) (#6840)

* fix(kiro): route Amazon Q runtime by profileArn region for cross-region IdC

Enterprise AWS IAM Identity Center accounts whose IdC instance lives outside the two
Amazon Q Developer profile regions (us-east-1 / eu-central-1) - e.g. eu-north-1
(Stockholm), start URL https://d-XXXX.awsapps.com/start - showed no limits and returned
502 on every request.

Root cause: the backend used the IdC/OIDC token region (providerSpecificData.region, e.g.
eu-north-1) for every CodeWhisperer runtime call, hitting q.eu-north-1.amazonaws.com - a
host that does not exist as a Q Developer runtime endpoint. Per AWS docs ("Supported
Regions for the Q Developer console and Q Developer profile"), the Q Developer *profile*
(which produces the profileArn and hosts generateAssistantResponse / GetUsageLimits /
ListAvailableModels / ListAvailableProfiles) is only hosted in us-east-1 and eu-central-1,
regardless of the IdC region; "data is stored in the Region where you create the Amazon Q
Developer profile."

Fix (new open-sse/services/kiroRegion.ts) decouples the two regions:
- providerSpecificData.region stays the IdC/OIDC region, used ONLY for
  oidc.{region}.amazonaws.com token mint/refresh.
- The runtime region is derived from the profileArn (resolveKiroRuntimeRegion):
  profileArn region -> a valid stored profile region -> us-east-1. A stored IdC region
  that is not a Q profile region (eu-north-1) is ignored for runtime.
- Profile discovery (discoverKiroProfileArnAcrossRegions) probes the Q profile regions
  (EU IdC -> eu-central-1 first) with the cross-region SSO token instead of q.{idcRegion}.

Wired into: executors/kiro.ts (generateAssistantResponse targets the profile region),
services/usage/kiro.ts (getKiroUsage multi-region discovery + profileArn runtime region so
Limits resolves), services/kiroModels.ts (ListAvailableModels), and
src/lib/oauth/providers/kiro.ts (login-time postExchange profile discovery).

Adds tests/unit/kiro-idc-cross-region.test.ts (15 cases). All Kiro suites pass (60 tests).

* fix(kiro): probe the IdC region too during profileArn discovery (any IdC region)

Make profile discovery general for an IdC in ANY of the ~30 IdC-supported AWS regions
(us-west-2, ap-southeast-2, me-central-1, af-south-1, ...), not just eu-north-1.

buildKiroProfileDiscoveryRegions now probes the two documented Q Developer profile regions
FIRST (us-east-1 / eu-central-1, EU-first for EMEA IdC regions to cut latency), then appends
the IdC/stored region itself as a forward-compatible fallback: if AWS ever co-locates the
profile with the IdC or expands the profile-region list, a same-region probe still finds it.
Probing a region with no profile simply returns nothing and we fall through. The profileArn's
own region remains authoritative for every runtime call (resolveKiroRuntimeRegion), so a
newly-issued ARN in any region is honored automatically.

Adds ap-southeast-2 (APAC) cross-region coverage and updates the discovery-order tests.

---------

Co-authored-by: artickc <artur1992123@mail.ru>

* feat(providers): manual context-window override for custom models (#4125) (#6822)

Add a manual per-model "Context Window Override" so an operator can correct
a provider's misreported context length (e.g. reports 1M when the real
limit is 128K) instead of the model getting silently dropped from combo
routing once the wrong value lands in the catalog.

Reuses the existing Feature-5004 model_context_overrides table
(source="manual") — already the priority-0 source getModelContextLimit()
(the function combo's context-window filter calls) reads ahead of the
models.dev/registry/static catalog — so no new resolver logic was needed,
only the missing write path:

- PUT /api/provider-models now accepts an optional contextWindowOverride
  (number to set, null to clear), persisted via setModelContextOverride/
  removeModelContextOverride.
- GET /api/provider-models surfaces the current override value + source
  back on each custom-model row.
- CustomModelsSection.tsx: edit form gained a Context Window Override
  input + a badge on the model row when an override is set.

Regression guard: tests/unit/provider-models-context-window-override-4125.test.ts
(manual override wins over a misreported catalog value, GET round-trip,
clearing via null, default-unchanged behavior).

* feat(dashboard): improve Provider Quota page horizontal density (#3520) (#6815)

QuotaCardGrid stacked every provider group vertically in a single
flex flex-col container, and each group's own card grid didn't go
multi-column until the md breakpoint. Provider groups now flow into a
2-column CSS multi-column layout on very wide (2xl) screens instead of
an unconditional vertical stack, and each group's card grid starts at
2 columns immediately, filling horizontal whitespace sooner on
narrower-but-not-mobile viewports.

Regression guard: tests/unit/quota-card-grid-horizontal-layout.test.ts

* refactor(usage): type saveRequestUsage with UsageEntry interface + any-budget ratchet (#3512) (#6809)

Replace saveRequestUsage(entry: any) with a typed UsageEntry interface
mirroring the usage_history columns 1:1. Fields stay optional/nullable
since different writers (chatCore success/failure, rejected-request
accounting, Codex Responses WS) populate the row incrementally; tokens
stays unknown since callers pass either raw provider-shaped usage or
the normalized {input,output,cacheRead,...} shape.

Also cleaned the file's other any usages (getUsageHistory filter,
getUsageDb next-cursor cast, appendRequestLog tokens param,
getRecentLogs catch) so it now sits at zero any and can be added to
the check:any-budget:t11 zero-any allowlist.

Documents the DB-entity <-> TS-interface convention in
docs/architecture/CODEBASE_DOCUMENTATION.md Sec 11.

* feat(combo): strict budget-cap fallback policy for auto/* combos (#3470) (#6816)

Auto-combo transparency + budget controls: the engine's budgetCap enforcement
always degraded to the globally cheapest candidate when every candidate
exceeded the cap - silently overspending instead of respecting the cap.

- engine.ts: budgetFallback "cheapest" (default, legacy) | "strict"
  (BudgetExceededError when no candidate fits budgetCap)
- requestControls.ts: X-OmniRoute-Budget-Fallback header +
  resolveRequestAutoControls() consolidating mode/budget/fallback parsing
- resolveAutoStrategy.ts / autoConfig.ts: thread combo-level
  config.budgetFallback and catch BudgetExceededError into an HTTP 402
- chat.ts: switch to the consolidated resolveRequestAutoControls() helper
  (net line reduction, stays under the frozen file-size baseline)

Regression guard: tests/unit/auto-combo-budget-fallback-3470.test.ts

* fix(usage): honor xAI provider-reported exact cost (#6711)

OmniRoute's calculateCost() always estimated request cost from token
counts x static pricing, discarding xAI's exact provider-reported cost
when present. xAI's chat-completions usage object reports the precise
billed cost via cost_in_usd_ticks (docs.x.ai/developers/cost-tracking
and the API reference's usage schema: "TICKS_IN_USD_CENT: i64 =
100_000_000" => 1e10 ticks/USD, e.g. 37756000 ticks ~= $0.0038).

calculateCost()/computeCostFromPricing() now short-circuit to this
exact figure when present -- before any pricing DB lookup, so it also
works for models without a local pricing row -- and still fall back to
the token-based estimate when it is absent. The field is threaded
through both the streaming (extractUsage/normalizeUsage) and
non-streaming (extractUsageFromResponse) usage-extraction paths.

Corrected divisor vs upstream: the upstream PR used /1e12 (a 100x
under-report, e.g. reporting $0.00123 as the doc's $0.123 example);
this port uses the doc-verified /1e10 instead, confirmed against both
the cost-tracking guide and the API reference's usage-object schema.

Inspired-by: https://github.com/decolua/9router/pull/2453

Co-authored-by: ryanngit <74137224+ryanngit@users.noreply.github.com>

* docs: rename /implement-prs → /merge-prs in Hard Rule #21 (skill renamed 2026-07-11) (#6847)

* docs: refresh stale llm.txt facts + relocate design.md to docs/architecture/DESIGN_SYSTEM.md (#6849)

* docs: refresh stale llm.txt facts + move design.md to docs/architecture/DESIGN_SYSTEM.md

llm.txt was frozen at the v3.8.8 era (177 providers, 37 MCP tools, 14
strategies, 9-factor scoring, 75% coverage gate). Update every factual
claim to the current state (248 providers, 94 tools / 30 scopes, 18
strategies, 12-factor scoring, ratchet + 60% floor, TS 6, current docs/
layout) and re-sync the 42 exact-copy i18n mirrors.

design.md at the root was a standardization plan whose phases 1-6 all
shipped; rewrite its header as a permanent reference and relocate it to
docs/architecture/DESIGN_SYSTEM.md per the root-hygiene policy (root =
configs + canonical docs only).

* docs: add MDX frontmatter to DESIGN_SYSTEM.md (in-app docs pipeline requires it)

* feat: per-model web-search interception rule (#3384) (#6814)

* feat(routing): per-model web-search interception rule (#3384)

Adds a per-provider/per-model interceptSearch rule (src/lib/db/interceptionRules.ts,
key_value namespace interception_rules) that overrides the existing native
web-search bypass defaults (Codex/Gemini/Claude->Claude passthrough) in
webSearchFallback.ts. Wired at the existing prepareWebSearchFallbackBody() call
site in chatCore.ts. Resolution precedence: per-model rule > provider-level rule
> existing native-bypass defaults.

This lands Phase 1-2 of the plan (rule store + search interception). Web-fetch
interception and the dashboard UI toggle are tracked as follow-up phases.

* fix(db): register interceptionRules in localDb re-export layer (db-rules gate)

* fix(db): renumber interception_rules migration 119→120 (collision with model_capability_overrides)

* feat: sidebar search/filter input (#4013) (#6810)

* feat(dashboard): add search/filter input to the dashboard sidebar (#4013)

Adds a search box at the top of the expanded sidebar that filters nav
sections/groups/items client-side by label, so users don't have to
hunt through the growing nav tree. Reuses the existing common.search /
common.noResults i18n keys (no new locale edits needed) and the shared
Input icon="search" pattern. Matching sections auto-expand while
searching and the accordion/pin state is restored once the query is
cleared.

Filtering logic is extracted into a pure filterSidebarSectionsByQuery()
helper (src/shared/utils/sidebarSearch.ts) so it is trivially unit
testable independent of React/next-intl/next-navigation.

* fix(test): move Sidebar.search test to a runner-collected path (test-discovery gate)

* fix(i18n): backfill 194 missing pt-BR keys (#6695) (#6723)

* fix(i18n): backfill 194 missing pt-BR keys and add key-parity regression test (#6695)

* Merge branch 'release/v3.8.47' into fix/6695-i18n-drift

Resolve i18n key-parity and CHANGELOG-fragment conflicts:
- Convert the #6695 CHANGELOG.md bullet to a changelog.d/ fragment
  (the fragment convention landed on release/v3.8.47 after this PR
  branched, per changelog.d/README.md).
- Backfill 61 additional pt-BR keys that entered en.json on
  release/v3.8.47 after this PR's original 194-key backfill, so the
  PR's own key-parity regression test (tests/unit/i18n-pt-br.test.ts)
  stays green against the moving release baseline.

* Discover live Codex models (#6776)

* Add live model discovery for provider catalog

* Fix model discovery request headers

* fix(codex): sync live model limits with local catalog

* test(codex): split live model discovery coverage into dedicated route tests

* fix(codex): use chatgpt account id for live model sync

* Add GitHub-backed Codex model discovery fallback

* fix(providers): tighten oauth config tests and provider model display comments

* test: align client version expectations with release default

* fix(codex): keep discovery complexity within baseline

* fix: rebase live Codex model discovery onto release/v3.8.47, preserving kimi-web buildHeaders (#6308)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* feat(codex): echo requested effort-suffixed model id in Responses payloads (#3697) (#6820)

* feat(codex): echo requested effort-suffixed model id in Responses payloads (#3697)

Codex CLI compatibility shim: the Responses API response.created/
response.in_progress/response.completed payloads now carry a `model`
field (previously absent), and for Codex-CLI-originated requests it
echoes the client-requested effort-suffixed model id (e.g.
gpt-5.5-xhigh) instead of the bare upstream id (gpt-5.5), so the Codex
CLI status line/model button shows the active reasoning effort.

- openai-responses.ts translator threads the upstream model into the
  Responses event objects (additive, omitted when unknown).
- New isCodexOriginatedHeaders() (codexIdentity.ts) reuses PR #3481's
  originator/User-Agent detection, header-based so it still fires when
  a combo routes codex/gpt-5.5-xhigh to a non-codex upstream.
- chatCore's existing opt-in #1311 echoModel pipeline now also fires
  automatically for Codex clients on the Responses API, regardless of
  the echoRequestedModelName setting.
- responseModelEcho.ts now also rewrites the nested response.model
  field the Responses API uses (previously only top-level model).
- /v1/models keeps returning models: [] for Codex (unchanged, #3481).

Regression guard: tests/unit/codex-effort-model-echo-3697.test.ts.

Closes #3697

* chore(merge): re-sync with release/v3.8.47 (restore CHANGELOG, keep own bullet)

* chore(merge): re-sync with release/v3.8.47; move changelog bullet to changelog.d fragment (merge-storm proof)

* feat(usage): surface Antigravity weekly quota alongside the 5-hour window (#4017) (#6818)

* feat(usage): surface Antigravity weekly quota alongside the 5-hour window (#4017)

Antigravity enforces both a 5-hour and a weekly usage limit, but the agy/antigravity
quota widget only exposed the 5-hour window. The weekly limit isn't in the per-model
retrieveUserQuota response already fetched — it lives in a separate, undocumented
retrieveUserQuotaSummary RPC that groups models into families (Gemini Models, Claude
and GPT models) with one weekly bucket per family.

Adds a self-contained usage/antigravityWeeklyQuota.ts leaf: a cached, best-effort
fetch of that RPC + a pure parser that extracts the weekly-labeled bucket per group
(window inferred from bucketId/displayName text, matching the reverse-engineered
shape documented by third-party Antigravity clients) into gemini_weekly/
claude_gpt_weekly quota entries, merged into the existing quotas map the widget
already renders generically. A failed/unavailable RPC never affects the existing
per-model quotas.

Live VPS validation attempt (192.168.0.15, real antigravity account): both
retrieveUserQuota and retrieveUserQuotaSummary currently return 429
RESOURCE_EXHAUSTED for that account, so the live response shape could not be
captured directly. The parser was instead validated via TDD against the bucket
shape documented by CodexBar (steipete/CodexBar), a third-party Antigravity client
that reverse-engineered the same RPC, and is defensive against both response
envelopes it has observed (top-level groups[] and nested quotaSummary.groups[]).

* chore(merge): re-sync with release/v3.8.47 (restore CHANGELOG, keep own bullet)

* chore(merge): re-sync with release/v3.8.47; move changelog bullet to changelog.d fragment (merge-storm proof)

* feat: add Z.ai Web free web-cookie provider (#4056) (#6823)

* feat(providers): add Z.ai Web free web-cookie provider (#4056)

New zai-web web-session provider drives the free chat.z.ai consumer
chat UI via a pasted browser cookie, distinct from the existing
API-key zai/glm/glm-cn/glmt providers (api.z.ai). ZaiWebExecutor posts
to chat.z.ai/api/chat/completions with the cookie forwarded both as
Cookie and Authorization: Bearer <token>, and normalizes both z.ai's
internal delta_content/phase SSE envelope and a pass-through
OpenAI-shaped choices[].delta frame into standard chat-completion
chunks.

Registered in WEB_COOKIE_PROVIDERS, WEB_SESSION_CREDENTIAL_REQUIREMENTS,
the provider registry (GLM-4.6/4.5/4.5V models), the executor factory,
and tokenExtractionConfig.ts for in-app cookie capture.

* fix(providers): regenerate translate-path golden for zai-web + reduce cognitive complexity

* fix(providers): rename ZaiWebExecutor.buildHeaders to avoid incompatible BaseExecutor override

* chore(merge): re-sync with release/v3.8.47; move changelog bullet to changelog.d fragment (merge-storm proof)

* fix(codex): bump default client version to 0.144.0 (#6780)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* refactor(usage): extract per-group parsing in antigravityWeeklyQuota (cognitive-complexity gate 886→885, release-level drift from #6818 merge)

* ci(quality): cut PR gate wall time without dropping protection (#6716)

Collapse duplicate CI spend while keeping each gate's existence reason:

- quality.yml: TIA __RUN_ALL__ defers full unit to fast-unit 4-shard (#6781);
  path filters via classify-pr-changes; docs-gates split; draft skip
- ci.yml: wire docs/i18n/code path filters; ESLint JSON artifact for quality-gate;
  drop advisory typecheck:noimplicit; float actions/cache@v6
- TIA parity: memory/usage/combo/serial; **/*.test.mjs any depth; electron/bin
  no longer force unit __RUN_ALL__
- check:complexity-ratchets: one ESLint walk, ruleId-isolated baselines + cache
- check:api-docs-refs + lib/apiRoutes: shared API route inventory
- husky pre-push: intentionally light (gates live in pre-commit); CLAUDE.md +
  QUALITY_GATES.md docs synced
- collect-metrics / lint:json: path.resolve cache path; Windows-safe eslint bin
- env-doc allowlist for ESLINT_RESULTS_JSON / COMPLEXITY_ESLINT_REPORT
- release-green --full-ci expects check:api-docs-refs (not docs-symbols alone)

Tests: select-impacted, classify-pr-changes, api-routes lib, complexity-rule-count,
validate-release-green.

Reconciled after #6781 (fast-unit 2→4 shards) per maintainer request on #6716.

Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>

* fix(docs): document Turbopack build memory tradeoff for RAM-constrained machines (#6409) (#6885)

* fix(routing): recognize Kimi token-limit 400 as context overflow for combo fallback (#6637) (#6893)

combo.ts's isContextOverflow400() guard required the literal word
'context' in the 400 error body before letting a combo fall through to
the next target. Kimi's exact wording ('Your request exceeded model
token limit: 262144 (requested: 308458)') never says 'context', so the
guard misclassified it as a body-specific error and halted the whole
combo instead of trying the next (larger-context) target.

accountFallback.ts's CONTEXT_OVERFLOW_PATTERNS already recognized this
wording one layer below (via checkFallbackError -> shouldFallback), so
the two independently-maintained classifiers disagreed and the
stricter one won. Export CONTEXT_OVERFLOW_PATTERNS from
accountFallback.ts and reuse it inside combo.ts's
isContextOverflow400() so both layers share a single source of truth.

Regression test: tests/unit/repro-6637-kimi-token-limit.test.ts
(RED on unfixed code -> GREEN after the fix). Existing #4519 guard
tests (tests/unit/combo-param-validation-fallback-4519.test.ts) still
pass, including the negative case that a genuinely body-specific 400
is NOT misclassified as overflow.

* fix(providers): honor a provider-level proxy assigned to no-auth providers (#6272) (#6895)

No-auth providers (mimocode, opencode, ...) are always dispatched with a single
hardcoded connectionId ("noauth" — SYNTHETIC_NOAUTH_CONNECTION_ID in
src/sse/services/auth.ts). No provider_connections row ever has id="noauth", so
resolveProxyForConnection() in src/lib/db/settings.ts could never populate
connectionRecord for them, and its provider-level proxy lookup (Steps 6/8) only
runs when connectionRecord is present. A proxy assigned via Settings -> Providers
-> mimocode was therefore silently ignored, reproducing the reporter's "same
thing happen when i set the proxy directly in the provider menu" symptom.

Adds a best-effort fallback (src/lib/db/settings/noAuthProxyFallback.ts): when
connectionRecord could not be resolved, scan the known no-auth provider ids for a
configured provider-level proxy (registry first, then legacy) before falling
through to the global/direct steps.

Regression test: tests/unit/proxy-noauth-provider-6272.test.ts (RED on unfixed
code — resolved to level=direct/proxy=null; GREEN after the fix).

* fix(dashboard): surface Claude extraUsage credits in quota card (#6806) (#6896)

Enterprise-tier Claude accounts (default_raven_enterprise) don't get
five_hour/seven_day utilization windows from Anthropic's OAuth usage
endpoint — only an extra_usage credit-billing block. parseClaude()
only read data.quotas, so quotas stayed {} and the dashboard showed
"No quota data" even when extraUsage showed the account 100%
exhausted. parseClaude() now folds an enabled extraUsage block into a
credits-style quota row (mirroring parseCodex's bankedResetCredits
pattern), both when quotas is empty and when it's already populated.

* fix(db): share sql.js preinit across callers, fix named-param bind (#6628, #6802) (#6899)

- preInitSqlJs() now memoizes an in-flight Promise (not just the resolved
  adapter) per filePath, so concurrent BATCH/STARTUP/HealthCheck/
  ProviderLimitsSync callers at boot share one full-file read+WASM decode
  instead of each independently reloading the whole database — the
  thundering-herd amplifier of the OOM condition #6632 already partly
  fixed, left un-implemented by the reporter's own proposed fix (#6628).

- sqljsAdapter's run/get/all now unwrap a lone named-parameter object
  (e.g. .all({ isActive: 1 }) for "WHERE is_active = @isActive", the same
  call shape getProviderConnections() already uses against better-sqlite3)
  before calling sql.js's stmt.bind(), expanding it to the @/:/$ sigil
  variants sql.js's own named-bind path requires. Previously the object
  was wrapped into an array and sql.js took the positional-bind path,
  throwing "Wrong API use : tried to bind a value of an unknown type
  ([object Object])." whenever the sql.js WASM fallback driver was active
  — exactly the error #6802 reported (misattributed to better-sqlite3).

Regression tests added to tests/unit/db-adapters/driverFactory.test.ts and
tests/unit/db-adapters/sqljsAdapter.test.ts, both proven RED against the
prior code and GREEN after the fix.

* fix(plugin): split OC-gate provider id from OmniRoute-facing routing id (#6859) (#6900)

resolveOmniRoutePluginOptions() auto-prefixes providerId with "opencode-"
(commit 75b52e286) so OpenCode 1.17.8+'s native-adapter gate accepts it as a
registered provider id. That prefixed value was being reused for the
OmniRoute-server-facing identifiers too: mapRawModelToModelV2's id/providerID,
mapComboToModelV2's providerID, and the dynamic provider hook's combo catalog
keys. OmniRoute's server has no "opencode-<x>" provider alias, so every
dispatched model failed credential lookup with "No credentials for
opencode-omniroute" / "No active credentials for provider:
opencode-omniroute".

Add a …

* test(ci): static body in codex e2e mock route bridge (CodeQL #737) (#7559)

CodeQL js/stack-trace-exposure flags ANY error-derived value returned in
the mock route bridge's 500 path, not just error.stack — swapping .stack
for error.message (in #7354, alert #736) left sibling alert #737 open on
the same line. Replace the body with a static string; the test only
asserts status===200, so the 500 body is never inspected. Clears the last
open CodeQL alert repo-wide, unblocking the Quality Ratchet on every PR.

Companion to the release/v3.8.49 PR (merge-gates §8 — gate/CI-touching fix
lands on main in the same session).

* fix(security): bump adm-zip >=0.6.0 + exact host matching in mitm DNS test (#7733)

* chore(deps): resolve 7 open Dependabot alerts via npm overrides (#8067)

- fast-uri ^3.1.3 (root + electron) — host confusion via IDN (#131/#126, high)
- hono ^4.12.27 — JSX ctx isolation / cx() XSS / v1 adapter req drop (#128/#129/#130, medium)
- @hono/node-server ^2.0.5 — serve-static path traversal (#127, medium); MCP uses only getRequestListener, not serve-static
- body-parser ^2.3.0 — DoS on invalid limit (#125, low)

Resolved: fast-uri 3.1.4, hono 4.12.31, @hono/node-server 2.0.11, body-parser 2.3.0. All clear in npm audit; lockfile-lint OK.

* chore(deps): resolve 3 Dependabot alerts (dompurify, fast-xml-parser, sharp) (#8070)

- dompurify ^3.4.12 (#132, low), fast-xml-parser ^5.10.1 (#133, high DOCTYPE), sharp ^0.35.0 (#134, high libvips)
Resolved: dompurify 3.4.12, fast-xml-parser 5.10.1, sharp 0.35.3. All clear in npm audit; lockfile-lint OK.

* docs(readme): use local SVG flags + convert doc-link tables to lists (#8317)

Flag emoji (regional-indicator) do not render on Windows and several
Safari/WebKit configs, breaking the 'In 42+ languages' table there.
Replace each emoji with a local SVG <img> from flag-icons (MIT), served
from docs/assets/flags/ so the language selector renders everywhere.

Also convert the 5 'Document | Description' reference tables to definition
lists — full-width and scroll-free on GitHub (tables are capped at
width:max-content and scroll horizontally on narrow viewports).

* fix(i18n): restore brand proper nouns and unify terminology in zh-CN and zh-TW

- Restore untranslated brand/model proper nouns (Claude, OpenAI, Anthropic, Gemini, MiniMax, etc.) in zh-CN and zh-TW
- Replace Chinese phonetic/translation forms (克劳德->Claude, 打开Ai->OpenAI, 人择->Anthropic, 双子座->Gemini, etc.)
- Unify 'provider' translation to '供应商'/'供應商' (was inconsistent: 提供者/服务商)
- Apply zh-TW localized terminology (網路/設定/檔案/新增/啟用/搜尋/儲存)
- Fix 'providers' value bug in zh-CN

* docs: sync env-var contract (chaos panel, notion TLS, grok auth path) + repair glued VNC line in .env.example (#8362)

* Add comparison and zero-config installation diagrams in SVG format

- Created a comparison table SVG illustrating the capabilities of OmniRoute versus competitors (9router, OpenRouter, CLIProxyAPI, LiteLLM) across 13 features.
- Added a zero-config installation SVG demonstrating the ease of setting up OmniRoute with three simple steps: installation, pointing to the tool, and receiving instant replies.

* fix(runtime): isolate unique 8177 repairs (#8298)

* perf(api): singleflight version lookups (#8278) (#8301)

Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>

* fix(providers): adapt Kimi nonstream requests internally (#8302)

Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>

* fix(mcp): keep POST SSE responses uncompressed (#8303)

Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>

* fix(cpa): isolate credential-pool failures (#8308)

* fix(cpa): isolate credential pool failures

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(cpa): forward transport through the chatCore key-health wrapper

The local recordKeyHealthStatus wrapper in handleChatCore only declared
(status, creds), so the transport argument added for CPA credential-pool
isolation was silently dropped at the call site (TS2554 "Expected 2
arguments, but got 3" once chatCore.ts is typechecked with tsc directly —
this file is not in tsconfig.typecheck-core.json's file list, so `npm run
typecheck:core` did not surface it). The CPA isolation guard in
keyHealth.ts never received `transport`, so it never fired.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(sse): suppress </think> by default on Chat Completions (#8245) (#8309)

Claude→OpenAI translation was emitting a literal </think> into
delta.content for ordinary Chat Completions clients. Reasoning already
ships as reasoning_content, so default to suppress and keep
x-omniroute-thinking-marker: on as the #4633 opt-in.

* fix(sse): preserve Responses combo payloads (#8310)

* fix(providers): classify HTTP 400 model-unavailable as MODEL_NOT_FOUND so Antigravity Pro fallback locks out the deprecated model (#8319)

* fix(providers): carve cookie-auth providers out of terminal 401 'expired' classification so one 401 cooldowns instead of killing the connection (#8321)

* fix(api): fold namespace into the flattened Chat tool name so cross-namespace leaves do not collide (#8322)

* fix(providers): classify per-model-quota 403 and DEGRADED 400 as model-unhealthy in checkFallbackError (#8247, #8248) (#8323)

* fix(providers): route noauth opencode-zen connections through their assigned proxy (#8324)

* fix(sse): re-export PROVIDER_BREAKER_FAILURE_STATUSES for the orphaned all-rate-limited breaker path (#8390)

Root cause: #8013 extracted shouldTripProviderBreakerForResult() from
src/sse/handlers/chat.ts into the new src/sse/handlers/chatPredicates.ts,
taking the (non-exported) const PROVIDER_BREAKER_FAILURE_STATUSES with it.
A second, independent use of that const survived in chat.ts's
handleSingleModelChat(), in the "all credentials rate-limited" block
(~line 1340) — that reference was left orphaned by the extraction.

Production impact: any request where every credential for a
provider+model is simultaneously rate-limited throws
`ReferenceError: PROVIDER_BREAKER_FAILURE_STATUSES is not defined` at
runtime in that code path. Concretely this meant:
- breaker._onFailure() was unreachable on the all-rate-limited path, so
  the provider circuit breaker could not trip from it
- the ReferenceError propagated up and got mapped to a generic 502,
  masking the real 503 upstream-unavailable status in combo responses
- the issue-agent route surfaced a generic 400 instead of the actual
  429 provider-rate-limited response

Fix: export PROVIDER_BREAKER_FAILURE_STATUSES from chatPredicates.ts and
add it to chat.ts's existing import block from that module. No behavior
change — the classification set ([408, 500, 502, 503, 504]) is
unchanged, this only repairs the broken reference.

Also re-points tests/unit/nvidia-quota-phase1.test.ts's regex-based
declaration check at chatPredicates.ts, where the const now actually
lives (it previously read chat.ts via fs+regex and silently failed to
find the declaration). The regex and the classification assertions
themselves are unchanged — this test still proves 429 is excluded from
the whole-provider breaker.

Refs #8013

* fix(sse): gate reasoning-placeholder strip to chunks that contain the sentinel (#8382)

Regression: #8162 (port of #8081) added an unconditional `.trim()` to
stripInternalReasoningPlaceholder(), applied to every streaming
delta.content chunk across 3 call-sites (openai-to-claude.ts,
openai-responses.ts, responsesTransformer.ts). Leading/trailing
whitespace at a chunk boundary is a real word boundary between
streaming fragments; trimming it glues adjacent chunks together on
the client ("Hello, " + "world." + " Bye." -> "Hello,world.Bye.").

Fix: early-return via .includes() before the replaceAll+trim, so the
function is a true no-op when the sentinel is absent from the chunk.
Behavior when the sentinel IS present is unchanged.

Validation:
- tests/unit/streaming-reasoning-dedup-5786.test.ts: the "(A-guard)"
  test was RED on the base branch ('Hello,world.Bye.' vs
  'Hello, world. Bye.'); GREEN after the fix (4/4 passing).
- tests/unit/translator-resp-openai-to-claude.test.ts: added a new
  multi-chunk boundary-whitespace regression test, proven RED against
  the pre-fix code (12/13), GREEN after (13/13).
- No regressions in responses-transformer.test.ts (17/17),
  responses-transformer-dense-output.test.ts (3/3), or the other
  suites exercising the shared placeholder utility (160/160 total
  across all consumers).

Refs #8162
Refs #8081

* fix(resilience): terminal-skip spares the recoverable GitHub Copilot no_refresh_token state (#8389)

Cause: #8182's terminal-connection guard in checkConnection() returns
early for any testStatus in {credits_exhausted, banned, expired} to
stop the sweep from wasting CPU/network probing connections that can
never self-heal. But testStatus="expired" + errorCode="no_refresh_token"
is exactly the state the pre-existing GitHub Copilot self-heal targets
(isGitHubAccessTokenOnlyConnection + canClearGitHubNoRefreshTokenState,
~line 83-97 / 413-492): a Copilot connection with no OAuth refresh
token but a still-valid copilotToken, which the sweep is supposed to
flip back to "active". With the new guard placed ahead of that block
unconditionally, the self-heal became unreachable for exactly the
state it exists to clear.

Impact: healthy GitHub Copilot connections that once lost their OAuth
refresh token got stuck at testStatus="expired" in the dashboard
forever, even though their Copilot sub-token kept working and the
sweep would have cleared the stale status back to "active" every
cycle before #8182.

Fix: carve out the exact recoverable shape from the terminal-skip
guard — testStatus==="expired" && errorCode==="no_refresh_token" &&
isGitHubAccessTokenOnlyConnection(conn) — so the guard still skips
every other terminal case (credits_exhausted, banned, and "expired"
for any other reason) untouched, matching #8182's original intent.

Validation: tests/unit/token-health-no-refresh-token-expired-5326.test.ts
was red (1 fail / 4 pass) before the fix — "checkConnection clears
stale no_refresh_token state for usable GitHub Copilot connections"
asserted testStatus flips back to "active" but got "expired". Green
after the fix (6/6, including a new boundary test proving a GitHub
Copilot connection expired for any OTHER reason, e.g. errorCode
"invalid_grant", is still skipped untouched). Also reran the adjacent
checkConnection/tokenHealthCheck suites (token-health-check.test.ts,
token-health-check-circuit-breaker.test.ts,
apikey-connection-health-check.test.ts, token-health-check-sweep.test.ts,
token-health-check-tickms-defined.test.ts, tokenHealthCheck-batchSize.test.ts,
codex-oauth-refresh-persist-6352.test.ts, oauth-providers-error-handling.test.ts)
— all green, confirming #8182's terminal-skip behavior is otherwise
unchanged. npm run typecheck:core clean.

Refs #8182
Refs #8286
Refs #5326

* fix(sse): cap exact cooldowns only when synthetic — verified upstream resets pass uncapped (#8393)

Contract vs cap: #6863 requires a model lockout to honor a VERIFIED
upstream quota reset exactly (e.g. Antigravity "Resets in 92h27m28s",
shipped in v3.8.47). #7940 requires SYNTHETIC exact-cooldown estimates
(the quota_exhausted until-midnight heuristic) to respect the
operator's maxCooldownMs so they cannot balloon unbounded. Both are
legitimate, non-conflicting contracts — they apply to different kinds
of values.

Root cause: #7980 (fixing #7940) changed recordModelLockoutFailure()
in open-sse/services/accountFallback.ts to unconditionally clamp
every exactCooldownMs against maxCooldownMs, with no way to
distinguish a verified upstream reset from a synthetic estimate. A
real ~92h reset got clamped to the operator's ~30min cap, and the
router went on hammering 429 against quota that was known not to
recover for days — regressing #6863's contract by omission, not by
new policy (the "honor it exactly" docstrings on
selectLockoutCooldownMs() and its call sites were left untouched and
now describe dead code).

Fix: add an opt-in `exactCooldownVerified` flag to
recordModelLockoutFailure()'s options. When true, exactCooldownMs
bypasses the maxCooldownMs clamp entirely; when false/omitted
(the default), behavior is byte-identical to before this change.
Set the flag only at the 4 call sites that already carry upstream
provenance for the value they pass — usedUpstreamRetryHint /
quotaResetHintMs from checkFallbackError():
  - open-sse/services/combo.ts (2 sites): exactCooldownVerified
    mirrors lockoutHintMs > 0, which is only ever nonzero when it
    traces back to a genuine upstream signal.
  - src/sse/services/auth.ts (2 sites): exactCooldownVerified mirrors
    the same usedUpstreamRetryHint / quotaResetHintMs check already
    used to derive exactCooldownMs at each site.
The quota_exhausted → until-midnight synthetic default and plain
exponential backoff are untouched and stay capped, per #7940. The
two other recordModelLockoutFailure call sites (combo.ts quality
failure, auth.ts local-404/grok-web-403) never carry a verified hint
and were left unmodified.

Validation (TDD): tests/unit/combo-lockout-quota-reset-6863.test.ts
red→green with its assertions unchanged (was clamping ~332,848,000ms
to ~1,799,995ms; now honors the parsed reset). Added a boundary pair
to tests/unit/model-lockout-exact-cooldown-cap.test.ts proving the
same magnitude resolves differently by provenance: synthetic stays
capped, verified passes through whole. Full existing suite in that
file plus combo-model-lockout-honors-reset-1308.test.ts stay green
unmodified. Swept 45 lockout/cooldown-adjacent test files (502/505
passing); the 3 failures reproduce byte-identical on a pristine
origin/release/v3.8.49 checkout (PROVIDER_BREAKER_FAILURE_STATUSES
ReferenceError in untouched chat.ts, and a documented timing-sensitive
serial test) — confirmed pre-existing, out of this fix's scope.
npm run typecheck:core and npm run lint are clean.

Refs #6863
Refs #7940
Refs #7980

* fix(sse): family auto-combos include any backend that serves the family (no-auth allowlist scoped to tier pools) (#8391)

Context: #8183 introduced AUTO_COMBO_NOAUTH_ALLOWLIST (opencode, felo-web) to
gate no-auth providers out of every auto/* candidate pool, motivated by public
HTTP egress reliability on the reference VPS (.15) — several no-auth backends
(duckduckgo-web, theoldllm, chipotle, aihorde) were flaky there. Its own tests
(noauth-autocombo-allowlist.test.ts, virtual-auto-combo.test.ts) never
exercised the auto/<family> path, so the gate silently applied there too.

auto/<family> combos (#6453, e.g. auto/glm, auto/zai) are a different axis:
an identity selector ("route to whatever genuinely serves GLM"), not a
reliability-curated pool. auggie (local CLI subprocess, zero HTTP egress —
the reliability concern #8183 targets doesn't even apply to it) advertises a
literal glm-5.2 model and had an explicit design-test seat in auto/glm since
#7032, but the #8183 allowlist silently excluded it from that pool.

Operator decision (2026-07-24): the no-auth allowlist gate keeps applying to
category/tier and flat-variant auto/* pools (auto/best-free, auto/coding:fast,
...), but auto/<family> pools bypass it — any no-auth backend that genuinely
serves the family is admitted.

Fix: thread a `bypassAllowlist` flag through isChatAutoComboNoAuthProvider()
and getNoAuthCandidates(), set to `Boolean(spec?.family)` at the single call
site in createVirtualAutoCombo(). Family narrowing (buildFamilyCandidateFilter)
still runs afterward, so a bypassed no-auth candidate only survives if its
model actually belongs to the requested family. Category/tier and flat-variant
pools (spec.family unset) keep the gate fully intact.

Validation:
- tests/unit/autoCombo/provider-family-combos.test.ts:136 was red (expected
  ["auggie","glm","zai"], got ["glm","zai"]) — now green (11/11 passing).
- tests/unit/noauth-autocombo-allowlist.test.ts (3/3) and
  tests/unit/virtual-auto-combo.test.ts (10/10, including "restricts the
  no-auth pool to the allowlist") stay green — the #8183 gate is untouched for
  spec-less/category/tier pools.
- Full tests/unit/autoCombo/ vitest sweep: 5 files, 36/36 passing.
- npm run typecheck:core clean.
- 4 unrelated failures pre-exist on origin/release/v3.8.49 (verified via `git
  show HEAD:<path>` swap, no stash) in
  tests/unit/auto-combo-credentialed-model-pool.test.ts (antigravity/gemini-3.5
  credentialed-pool logic, untouched by this change).

Refs #8183, Refs #6453, Refs #7032

* fix(compression): rank codex-responses in adaptive-ladder maps (#8381)

#8010 registered the codex-responses engine in the compression catalog
(engineCatalog.ts, stackPriority 12 between rtk's 10 and headroom's 15)
but never added it to adaptiveCompression/ladder.ts's AGGRESSIVENESS and
REDUCTION_FACTOR maps. Those maps' own header documents that they must
cover every real catalog/registry engine, not just the 7 in
DEFAULT_LADDER, so an operator adding codex-responses via ladderOverride
silently fell back to aggressivenessOf() === 0 (same as "off") and
expectedReductionFactor() === 0.9 (the generic default), breaking
floor-mode escalation ranking for any ladder that includes it.

Add "codex-responses" to both maps between rtk and ionizer, matching
its stackPriority (12) sitting between rtk's (10) and ionizer's (13):
- AGGRESSIVENESS: 22 (between rtk's 20 and ionizer's 25)
- REDUCTION_FACTOR: 0.84 (between rtk's 0.85 and ionizer's 0.83),
  reflecting its "lossless-first, bounded diagnostic" guidance in
  engineCatalog.ts

Validation: tests/unit/ladder-engine-maps-6533.test.ts red -> green
(2 of 3 tests were failing on the missing engine; all 3 pass after the
fix). Sanity-checked neighbors compression-exclusions.test.ts and
compression/adaptive-resolve-plan.test.ts still pass.

Refs #8010

* fix(i18n): restore #8219 CacheSettingsTab key sync + synthetic fixture for zh-TW repro test (#8387)

Root cause (two independent causes):
1. PR #8219 (commit 2a865aaaa7) added CacheSettingsTab.tsx with 12
   t("settings.*") calls whose keys were never created anywhere, not even
   in en.json (the source of truth). The same PR added only 3 sidebar/header
   keys (settingsCache, settingsCacheSubtitle, settingsCacheDescription) to
   en+es, without running `npm run i18n:sync-ui` to propagate to the other
   41 locales.
2. tests/unit/i18n-missing-placeholder-fallback.test.ts had a "#7258 repro"
   test asserting the real zh-TW.json still carried raw __MISSING__:
   placeholders — a premise invalidated by #8024, which completed the
   Traditional Chinese translation to 100%.

What changed:
- Added the 12 missing settings.* keys to en.json, mirroring the sibling
  requestBodyLimit* family (placeholders {min}/{max}/{value} match the
  component exactly).
- Added real, natural translations for all 15 CacheSettingsTab-related keys
  (12 settings.* + 3 sidebar/header) to pt-BR.json, vi.json and es.json (es
  already had the 3 sidebar/header keys).
- Ran `npm run i18n:sync-ui` (official tool, no locale hand-edited) to stub
  the remaining 39 locales with __MISSING__:<english>. This also discovered
  17 pre-existing unrelated missing keys (compression-exclusions settings,
  8 new-provider onboarding descriptions) never synced since #8031, and
  pruned 3 dead orphaned zh-TW-only keys (codexSessionAffinity{Title,Desc,
  Ttl}, superseded by the generic sessionAffinity* keys since #7274,
  confirmed unused anywhere in src/) — verified programmatically as
  +32/-0/~0 changed per stub locale, +32/-3/~0 changed for zh-TW.
- Rewrote the "#7258 repro" test to use a synthetic fixture (same style as
  the sibling deepMergeFallback fixtures in the same file) instead of
  depending on zh-TW.json's real, evolving translation-completeness state.
  Proves the same behavior: collectPlaceholderLeaves() detects a raw
  __MISSING__: leaf before deepMergeFallback (the fix) is exercised.

Validation: all 4 previously-red files green (23/23 assertions). Broader
sweep of 271 i18n-adjacent unit tests unaffected. i18n:check-ui-coverage
(42/42 locales >=80%, 99.7-100%) and i18n:check-glossary both pass.
typecheck:core clean.

Refs #8219
Refs #8024

* chore(quality): drop stale muse-spark-web allowlist entry + sync sidebar order snapshots (#8383)

Two independent "code is right, bookkeeping lagged" base-reds:

1. #8233 made open-sse/executors/muse-spark-web.ts import
   sanitizeErrorMessage from utils/error.ts (a real Rule #12 fix), but
   left its KNOWN_MISSING_ERROR_HELPER allowlist entry in
   scripts/check/check-error-helper.mjs in place. The gate's own
   stale-allowlist enforcement (assertNoStale) correctly flagged the now
   -obsolete entry: `npm run check:error-helper` failed with "1 entrada(s)
   obsoleta(s)", and tests/unit/check-error-helper.test.ts's "the shipped
   allowlist freezes exactly the known current violators" test expected
   an empty Set. Removed the entry (kept the assertNoStale machinery and
   the general scope-header comments untouched).

2. #8064 added the "compression-exclusions" sidebar item right after
   "compression-studio" in COMPRESSION_CONTEXT_GROUP (deliberate,
   complete feature) but didn't update two order-snapshot tests written
   before that item existed:
   - tests/unit/sidebar-visibility.test.ts expected the "omni-proxy"
     section's flattened id list to end the compression block at
     "compression-studio".
   - tests/unit/ui/sidebar-engine-items.test.ts asserted "Studio must be
     last" in COMPRESSION_CONTEXT_GROUP.
   Updated both to the real, intentional order: Settings -> Combos ->
   engines -> Studio -> Exclusions (Studio now second-to-last,
   Exclusions last).

Validation (red -> green):
- check:error-helper gate: red ("1 entrada(s) obsoleta(s)") -> green
  ("OK (898 files scanned, 0 known-missing frozen)")
- tests/unit/check-error-helper.test.ts: 31/32 -> 32/32
- tests/unit/sidebar-visibility.test.ts: 6/7 -> 7/7
- tests/unit/ui/sidebar-engine-items.test.ts: 13/14 -> 14/14

Refs #8233
Refs #8064

* test: realign catalog snapshot tests to current deliberate catalog state (#8386)

* test: realign catalog snapshot tests to current deliberate catalog state

Six catalog/snapshot tests drifted behind deliberate catalog changes that
were already validated by newer sibling tests. No production code touched;
every change aligns a stale snapshot to behavior already validated by newer
sibling tests.

Root causes (all confirmed against the current code before editing):

- tests/unit/providers-constants-split.test.ts: APIKEY_PROVIDERS grew from
  187 to 195 entries via #8077 (clova-studio/internlm/ant-ling, regional),
  #8161 (sarvam/plamo → regional, writer → frontier-labs) and #8170
  (typhoon → regional, inception → frontier-labs). Family counts verified
  to sum to 195 (gateways 60, frontier-labs 24, inference-hosts 28,
  enterprise-cloud 17, regional 40, specialty-media 26) with no duplicates.
  Updated the two assertions and extended the changelog comment.

- tests/unit/qianfan-provider.test.ts: the expected Baidu Qianfan website
  URL was the pre-#8128 wenxinworkshop path. #8128/#6271 moved it to
  https://cloud.baidu.com/product-s/qianfan_home, already locked by the
  sibling regression test tests/unit/baidu-qianfan-website-urls-6271.test.ts.

- tests/unit/t31-t33-t34-t38-model-specs.test.ts and
  tests/unit/auto-combo-credentialed-model-pool.test.ts: the Antigravity
  catalog refactor (#8013) retired gemini-3-pro-preview/claude-sonnet-5 and
  renamed the Gemini 3.5 Flash tiers (low/medium/high ->
  extra-low/low/gemini-3-flash-agent), confirmed against
  ANTIGRAVITY_PUBLIC_MODELS and tests/unit/antigravity-retired-public-models.test.ts.
  Swapped the retired IDs for currently-registered ones
  (gemini-3.6-flash-high, claude-sonnet-4-6, gemini-3-flash-agent,
  gemini-3.5-flash-low/extra-low) and moved the wildcard-exclusion prefix
  test from the now-2-tier "gemini-3.5-*" group to "gemini-3.6-*", which has
  3 real tiers today (same >=3 semantics, just pointed at a prefix that
  still has 3 members).

- tests/unit/model-alias-seed.test.ts: getModelInfo("gemini-3.1-pro") now
  canonicalizes through ALIAS_TO_PROVIDER_ID["agy"] = "antigravity" (#8050),
  the same pattern already applied to opencode -> opencode-zen. Updated the
  expected provider id.

- tests/unit/video-dashscope.test.ts (deleted, 216 lines): #8266
  reorganized the Alibaba video catalog so the flat wan2.7-t2v id no longer
  exists under the plain "alibaba" provider (only the dated
  wan2.7-t2v-2026-06-12 does); the flat id now lives only under
  "qwen-cloud". All 6 tests in the file failed because they built  requests
  against alibaba/wan2.7-t2v, which the new allowlist now rejects with 400
  ("unsupported alibaba video model") - verified directly against
  VIDEO_PROVIDERS in open-sse/config/videoRegistry.ts. Coverage already
  exists and was confirmed passing pre-deletion in
  tests/unit/alibaba-video-media.test.ts (including an explicit
  "Alibaba rejects video models outside its own allowlist" case for this
  exact id) and tests/unit/qwen-cloud-video-media.test.ts (covers the same
  id under qwen-cloud). Note: the deleted file's DashScope upstream
  error-path assertions (401 missing credentials, 502 missing task_id, 502
  FAILED status, 504 poll timeout) don't have a byte-for-byte equivalent in
  the two replacement files, though the shared dashscopeHandler.ts code
  path they exercise remains covered by several sibling *-media.test.ts
  files for the happy path and local validation.

- tests/unit/authz/spawn-capable-prefixes-client-safe.test.ts: #7892 added
  /api/vnc-session to the SPAWN_CAPABLE_PREFIXES deny-list (Hard Rules
  #15/#17 hardening). Bumped the expected length 10 -> 11 and added the
  entry to the test's named list for documentation.

Refs #8013, #8050, #8266, #7892, #8128

* chore(quality): allowlist the video-dashscope.test.ts deletion with its replacements

check:test-masking (pr-test-policy CI gate) requires a _deletedWithReplacement
entry for any deleted test file, even when the deletion is a verified-legitimate
supersession. Documents the same #8266 rationale from the prior commit in the
machine-checked allowlist so the deletion is not flagged as unexplained masking.

Refs #8266

* test: hermetic notion thread-session mocks + drop duplicated usage-analytics suite (#8392)

* test: hermetic TLS mock for notion thread-session suite + drop duplicated usage-analytics file

Root cause A (#8159): sendNotionInferenceRequest() in
open-sse/executors/notion-web.ts was migrated from fetch() to
tlsFetchNotion() (open-sse/services/notionTlsClient.ts, native
tls-client-node binary) to get past Notion's Cloudflare TLS
fingerprinting. #8159 updated the mock in the sibling
tests/unit/executor-notion-web.test.ts (installNotionTlsMock, wired
through __setTlsFetchOverrideForTesting) but never touched
tests/unit/executor-notion-web-thread-sessions.test.ts (split out
earlier by #7900) — its 3 execute()-driven tests still mocked
globalThis.fetch, which tlsFetchNotion() never calls once the native
TLS client loads successfully. Confirmed live: all 3 tests hit real
https://app.notion.com with a fake cookie and got a real 401
(~8.1-8.5s each here; on a network with blocked/slow egress this
would instead hang up to the client's ~190s timeout+grace per test —
a CI-hang risk).

Fix: replicate installNotionTlsMock verbatim from the sibling file
into executor-notion-web-thread-sessions.test.ts so the 3 tests mock
the TLS override point instead of global fetch. Suite is now fully
hermetic — 8/8 pass, no network I/O, total runtime 31.0s -> 8.0s.

Root cause B (#7700): tests/unit/usage-analytics-route-extra.test.ts
was created as a byte-for-byte duplicate of 10 of the 22 tests in
tests/unit/usage-analytics-route.test.ts. #7300 later fixed a fixture
bug in the retention-window boundary test ("does not double-count raw
and aggregated rows") in the main file only — reading
getUserDatabaseSettings().retention.usageHistory live instead of a
hardcoded 30-day cutoff (default retention is 365 days) — leaving the
duplicate copy on the stale hardcoded value, which now fails (1 !== 2).

Fix: delete the duplicate file. All 10 of its test names exist
verbatim in the main file (verified with comm -12) and that file
passes 22/22:
  - does not double-count raw and aggregated rows
  - does not persist guessed API key attribution
  - does not throw Unknown named parameter on short range (needsAggregated=false)
  - does not throw Unknown named parameter with apiKey filter on long range
  - groups renamed API key usage by stable ID
  - includes activityMap for heatmap
  - includes cost by API key
  - omits global aggregates when filtering by API key
  - returns 500 on database errors
  - returns weeklyPattern for the costs dashboard
No coverage loss — same production code, same assertions, one fewer
redundant file.

Validation:
  - RED executor-notion-web-thread-sessions.test.ts: 5 pass / 3 fail
    (401 !== 200, real network hit), 31.0s
  - RED usage-analytics-route-extra.test.ts: 9 pass / 1 fail
    (1 !== 2), 18.7s
  - GREEN executor-notion-web-thread-sessions.test.ts: 8/8 pass, 8.0s,
    hermetic (no network)
  - GREEN executor-notion-web.test.ts (sibling, untouched): 37/37
    pass, byte-identical diff
  - GREEN usage-analytics-route.test.ts (untouched): 22/22 pass,
    byte-identical diff
  - npx eslint on the changed file: clean
  - npm run typecheck:core: clean (exit 0)

Refs #8159
Refs #7300
Refs #7700

* chore(quality): register usage-analytics-route-extra deletion in test-masking allowlist

check:test-masking hard-flags any deleted test file without a
_deletedWithReplacement entry. The deletion is legitimate (100% duplicate
suite, coverage retained verbatim in tests/unit/usage-analytics-route.test.ts)
-- same registration pattern as the video-dashscope entry.

Refs #7700
Refs #7300

* chore(ci): cancel superseded runs, skip DAST on docs-only PRs, persist TIA shadow evidence (#8379)

Runner-cost pass grounded in the #8084 review of the current pipeline:

- dast-smoke.yml: add concurrency cancel-in-progress (25-min advisory builds were
  stacking on force-push storms) and paths-ignore for docs/**+**/*.md — a docs-only
  PR cannot change DAST behavior but was paying the 6-11min CLI-bundle build.
- semgrep.yml: add concurrency cancel-in-progress. No paths filter on purpose:
  p/secrets must keep scanning docs-only diffs (credentials leak in .md too).
- quality.yml (TIA step): persist the per-PR impacted-test selection to a
  tia-selection artifact + GITHUB_STEP_SUMMARY line. This is the shadow-evidence
  phase: TIA false negatives become measurable against fast-unit's full-suite
  verdict across releases BEFORE any gate authority moves off ordinary PRs.

Refs #8084

* docs: one golden path across PR template, CONTRIBUTING, GEMINI, AGENTS + CI milestones in ROADMAP (#8380)

Contributor guidance contradicted itself in four places (found in the #8084 review):

- pull_request_template.md + CONTRIBUTING.md asked contributors to run the FULL
  unit suite + coverage gate locally, while the maintainer's stated golden path
  (#8273/#8329) is: focused tests for the change locally; full suite, coverage,
  and build are CI's job. On 16GB hosts the full local chain has saturated
  machines (#8084 incident report).
- GEMINI.md demanded coverage >= 75/75/75/70 while the official CI gate is
  60/60/60/60 (quality-baseline ratchet on top).
- AGENTS.md fork workflow said to branch from upstream/main; the default branch
  is the active release/vX.Y.Z line (main only receives release squash-merges).

Also makes the #8084 CI direction explicit in the public ROADMAP: lane
consolidation (3.8.51), one CI policy for release/** and main (3.8.52),
full-regression authority -> merge queue after TIA shadow evidence (3.8.54),
preview-artifact + build-once rehearsal inside the 3.8.58 dry-run.

Refs #8329
Refs #8084

* fix(translator): cap thinking budget on explicit budget_tokens path (#8312)

* fix(translator): cap thinking budget on explicit budget_tokens path

* fix(translator): stop dropping thinkingConfig on cap-0 reasoning_effort path

The thinking-budget-cap guard added in this branch skipped thinkingConfig
entirely whenever a model's thinkingBudgetCap was 0 (e.g. gemini-3-flash),
including on the reasoning_effort/budgetMap path. That regressed the
pre-#6943 native-defaults contract (thinkingBudget 0 / includeThoughts
false must still be present) and crashed callers that read
`.thinkingConfig.thinkingBudget` unconditionally
(translator-openai-to-gemini-defaults.test.ts).

Also restore includeThoughts:true on the Claude-format explicit
thinking.budget_tokens path (openai-to-gemini.ts's Claude-format field and
claude-to-gemini.ts's native thinking field): budget_tokens:0 there is the
client's dynamic-thinking sentinel (#6813), not an off-switch, and must
stay true even after the new capping — the cap must only clamp positive
explicit values, never flip the zero sentinel's semantics.

Updates two tests this branch added that encoded the incorrect
"omit thinkingConfig / includeThoughts:false for the 0 sentinel" behavior,
to match the pre-existing, still-required contracts above.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(providers): revert scope-creep flip of Gemini 3.5/3.6 Flash supportsThinking

The thinking-budget-cap fix accidentally expanded 5 shorthand modelSpecs
entries (gemini-3.5-flash, gemini-3.5-flash-low, gemini-3.6-flash-high/
medium/low) into explicit objects setting supportsThinking:true and
thinkingBudgetCap:24576. That flip was unrelated to the two proven test
regressions (translator-openai-to-gemini-defaults.test.ts and
claude-to-gemini-budget-tokens-zero-6813.test.ts, which only exercise
gemini-3-flash-preview, gemini-3.1-pro and gemini-2.5-pro) and reopens a
deliberately closed path from #8013: Antigravity still rejects
client-supplied thinking params for these Gemini 3.5/3.6 Flash tier ids,
so supportsThinking must stay false (inherited from
GEMINI_35_FLASH_MODEL_SPEC).

Reverted all 5 entries back to the release shorthand
`{ ...GEMINI_35_FLASH_MODEL_SPEC }`. gemini-3-flash, gemini-3.1-pro and
gemini-2.5-pro (the models the regression tests actually exercise) were
already correctly specced in the release baseline and are untouched.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(providers): reconcile Kimi K3 vision when attachment contradicts modalities (#8250) (#8313)

* fix(providers): reconcile Kimi K3 vision when attachment contradicts modalities

Synced models.dev rows for kimi-coding*/k3 can ship attachment=false while
modalities_input still lists image/video. Prefer the modality signal (and
normalize at sync + resolve) so supportsVision, attachment, and exposed
modalities agree.

Closes #8250

* fix(providers): keep Kimi K3 static fallback text-only (#8250)

The Kimi K3 vision reconciliation added supportsVision=true directly
to the kimi-coding registry entry for id "k3". That entry is the
static/stable fallback catalog used when discovered capabilities are
unavailable, and it must stay text-only per #4071 — the vision fix is
already applied correctly on the discovered path via
MODEL_SPECS["kimi-k3"] (aliases: ["k3"]) and
modelCapabilities.ts::resolveVisionCapability.

Restores the invariants guarded by
tests/unit/kimi-k2.7-code-registration.test.ts ("Kimi Code k3 fallback
leaves discovered capabilities unset") and
tests/unit/catalog-updates-v3829-kimi-qwen.test.ts ("kmca stable
fallback only carries documented static capabilities").

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* docs: update provider icons and enhance API interface documentation

* docs: update section headers in README for improved clarity

* docs: improve formatting and structure in README for better readability

* feat(opencode-plugin): auto-discover models while running + force sync (#8101)

* feat(opencode-plugin): auto-discover models while running + force sync

Add Pi-parity discovery for OpenCode:
- autoSyncIntervalMs background refresh (default 5m, min 60s, 0=off)
- omniroute_sync_models tool to force cache invalidate + /v1/models refetch
- /omni-sync and /omni-autosync command templates (OpenCode has no slash API)

* docs(opencode-plugin): document auto model discovery + /omni-sync

Document Pi-parity catalog refresh for OpenCode:
- autoSyncIntervalMs background discovery (default 5m)
- omniroute_sync_models force-refresh tool
- /omni-sync and /omni-autosync command templates

---------

Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>

* chore(deps): bump next to 16.2.11 (9 security advisories) (#8265)

Closes 9 Dependabot alerts (#135-#143) — Next.js 16.0.0..<16.2.11:
SSRF in Server Actions/rewrites, cache confusion, DoS (Server Actions,
Image Optimization SVG, Edge payload), middleware/proxy bypass, and
unauthenticated Server Function endpoint disclosure.

Lockfile bump within the existing ^16.2.6 range (now floored at
^16.2.11); no production code touched.

Co-authored-by: rafaumeu <rafael.zendron22@gmail.com>

* fix(antigravity): add missing gemini-3.6-flash pricing rows to ag OAuth pricing (#8290)

release/v3.8.49 already ships the Gemini 3.6 Flash catalog entries
(AGY_PUBLIC_MODELS, ANTIGRAVITY_PUBLIC_MODELS, MODEL_SPECS with
supportsThinking: false — Antigravity still rejects client-supplied
thinking params) via #8013. What was still missing: the `ag` pricing
rows in DEFAULT_PRICING_OAUTH, so getPricingForModel("ag", id)
returned null for the three tiers and cost/quota calculations
silently fell back to $0.

Pricing: $1.50 input / $7.50 output / $0.15 cached per MTok (Google's
2026-07-21 announcement), matching the existing 3.5-flash schedule
shape. Thinking tokens billed at output rate.

Extends the existing pricing-ag-flash-tiers.test.ts (RED-first: all
three tiers failed the "non-null pricing row" assertion before this
change) rather than re-adding the already-shipped catalog/modelSpecs
entries.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* feat(combo): enforce provider and model family invariants (#8304)

* feat(combo): enforce provider and model family invariants

Closes #8279

Co-Authored-By: Ravi Tharuma <ravitharuma@users.noreply.github.com>

* fix(combo): complete invariant enforcement paths

Map invariant failures to structured API errors, correct target diagnostics, and validate restored combos inside the existing migration transaction.

Co-Authored-By: Ravi Tharuma <noreply@github.com>

---------

Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>
Co-authored-by: Ravi Tharuma <noreply@github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(memory): self-heal upsertVector/deleteVector from a raced vec_memories drop (#8337)

Live incident: memory.vec.upsert.fail {"error":"no such table: vec_memories"}
recurred repeatedly right after restarts, even though ensureReady() is called
immediately beforehand. ensureReady()'s signature-check-then-maybe-recreate
logic (resetForSignature does DROP TABLE IF EXISTS + CREATE VIRTUAL TABLE) is
not synchronized against a concurrent caller's upsertVector/deleteVector -- a
second in-flight memory write that independently decides (from a stale read
of memory_vec_meta) it also needs to reset the table can drop it out from
under another write's insert. Confirmed live: memory_vec_meta showed
vec_loaded=0 for the entire session across many restarts, then flipped to 1
mid-investigation once one attempt finally completed without interruption --
consistent with an intermittent race, not a permanently broken path (verified
the underlying sqlite-vec extension and CREATE VIRTUAL TABLE statement work
correctly in isolation, both on the host and inside the production container).

Rather than chase the exact interleaving (every underlying SQLite call is
synchronous via better-sqlite3, so the race window is narrow and did not
reproduce under simple Promise.all stress tests), makes the write path
resilient to arriving after the table was dropped: on a "no such table"
error, recreate vec_memories from the last-known-good memory_vec_meta
dimension and retry once.

Co-authored-by: Markus Hartung <markus.hartream@gmail.com>

* fix(sse): stop stripInternalReasoningPlaceholder from eating inter-word spaces (#8341)

Live incident: streamed assistant text was losing the spaces BETWEEN words
(e.g. "Bilden är en riktig JPEG nu" -> "Bildenärenriktig JPEG nu") on the
Responses-API and Claude streaming paths.

stripInternalReasoningPlaceholder() (#8081/#8162) is called on every
individual delta.content chunk, and unconditionally called .trim() even when
its sentinel ("(prior reasoning summary unavailable)") was never present in
that chunk. Tokenizers commonly emit sub-word tokens with a leading space as
part of the token (e.g. " en", " riktig") -- each such chunk got its only
whitespace character (the inter-word space) silently trimmed away before
being appended to the accumulated message, while the words themselves stayed
intact. Punctuation-only chunks were largely unaffected, matching what was
observed live.

Only trims when the sentinel is actually present -- preserves the original
#8081 intent (collapse a placeholder-only chunk to "") without touching the
overwhelming majority of chunks that never contain it.

Co-authored-by: Markus Hartung <markus.hartream@gmail.com>

* fix(dashboard): show custom provider_nodes providers in the Topology view instead of only AI_PROVIDERS (#8328) (#8357)

* fix(sse): strip third-party-agent signals from the Hermes system prompt that trigger Anthropic 400 extra-usage (#8350) (#8358)

* fix(api): accept the current compatible-provider connection id scheme in the models test route (#8326) (#8359)

* fix(backend): keep combo routing from dispatching image requests to text-only targets (#8332) (#8360)

* fix(api): stop leaking the internal provider UUID in /v1/models and honor the configured prefix (#8327) (#8361)

* refactor(sse): classify SSE critical-path empty catches + add CONTRIBUTING convention (#8142) (#8364)

* fix(api): stop the 2000-token safety buffer from inflating usage.prompt_tokens in the client response (#8331) (#8356)

* fix(api): stop the 2000-token safety buffer from inflating usage.prompt_tokens in the client response (#8331)

* fix(sse): scope #8331's usage-buffer fix around Claude-Code-compatible providers

The #8331 fix correctly stopped folding the 2000-token context-window safety
margin into client-visible prompt_tokens/input_tokens/total_tokens for normal
API metering clients. But it also silently changed the response shape for
Claude-Code-compatible providers, whose own context accounting reads the
buffered number straight out of usage — regressing
tests/unit/cc-compatible-provider.test.ts (expected 2007, got 7).

Fold the computed context_budget_* fields back into the visible usage fields
for that one path only (applyClientUsageBuffer's new
preserveContextBudgetInVisibleUsage option, gated on the existing
isClaudeCodeCompatible flag in chatCore.ts). Every other caller keeps the
real, unbuffered #8331 numbers.

* docs: enhance README formatting with tables for better structure and readability

* docs: enhance README with tables for improved structure and readability

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: growab <nekron@icloud.com>
Co-authored-by: KooshaPari <42529354+KooshaPari@users.noreply.github.com>
Co-authored-by: Chirag Singhal <76880977+chirag127@users.noreply.github.com>
Co-authored-by: Chirag Singhal <chirag127@users.noreply.github.com>
Co-authored-by: Ronaldo Davi <ronaldodavi@gmail.com>
Co-authored-by: Andrew Munsell <andrew@wizardapps.net>
Co-authored-by: WITALO ROCHA <witalo_rocha@hotmail.com>
Co-authored-by: Wital <witalorocha216@gmail.com>
Co-authored-by: Aoxiong Yin <i@yinaoxiong.cn>
Co-authored-by: Andrew B. <37745667+AndrianBalanescu@users.noreply.github.com>
Co-authored-by: Andrian B. <andrewbalanesq@gmail.com>
Co-authored-by: Xiangzhe <32761048+xz-dev@users.noreply.github.com>
Co-authored-by: Jon Bailey <297513015+Pitchfork-and-Torch@users.noreply.github.com>
Co-authored-by: Pitchfork-and-Torch <Pitchfork-and-Torch@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: Samir Abis <me@samirabis.com>
Co-authored-by: lucasjustinudin <34107354+lucasjustinudin@users.noreply.github.com>
Co-authored-by: chy1211 <31048289+chy1211@users.noreply.github.com>
Co-authored-by: ryanngit <74137224+ryanngit@users.noreply.github.com>
Co-authored-by: whale9820 <whale9820@users.noreply.github.com>
Co-authored-by: anhdiepmmk <n08ni.dieppn@gmail.com>
Co-authored-by: Septianata Rizky Pratama <19322988+ianriizky@users.noreply.github.com>
Co-authored-by: NOXX - Commiter <artur1992123@mail.ru>
Co-authored-by: backryun <bakryun0718@proton.me>
Co-authored-by: lunkerchen <labanchen@gmail.com>
Co-authored-by: lunkerchen <lunkerchen@users.noreply.github.com>
Co-authored-by: Ray Doan <raydoan.contact@gmail.com>
Co-authored-by: kfiramar <83420275+kfiramar@users.noreply.github.com>
Co-authored-by: Jan Leon <Jan.gaschler@gmail.com>
Co-authored-by: Someres <168349709+quanturbo@users.noreply.github.com>
Co-authored-by: MikeTuev <ra9ftm@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Imam Wahyu Widodo <120608486+hajilok@users.noreply.github.com>
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Co-authored-by: AgentKiller45 <jamalzzj45@gmail.com>
Co-authored-by: judy459 <JUDYZHU459@outlook.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>
Co-authored-by: Markus Hartung <mail@hartmark.se>
Co-authored-by: KooshaPari <koosha@phenotype.io>
Co-authored-by: Jade Guo <jade.gly@gmail.com>
Co-authored-by: Paijo <14921983+oyi77@users.noreply.github.com>
Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
Co-authored-by: Dayna Blackwell <dayna@blackwell-systems.com>
Co-authored-by: backryun <backryun@daonlab.local>
Co-authored-by: brick30llc-ctrl <brick30llc@gmail.com>
Co-authored-by: brick30llc-ctrl <admin@brick30.com>
Co-authored-by: Saren <saren@dumstruck.com>
Co-authored-by: Rafael Dias Zendron <mmmarckos@gmail.com>
Co-authored-by: Xiangzhe <xiangzhedev@gmail.com>
Co-authored-by: Rafael Dias Zendron <rafael.zendron22@gmail.com>
Co-authored-by: KooshaPari <62650152+KooshaPari@users.noreply.github.com>
Co-authored-by: Wibias <37517432+Wibias@users.noreply.github.com>
Co-authored-by: huohua-dev <celentanohertor@gmail.com>
Co-authored-by: huohua-dev <258873123+huohua-dev@users.noreply.github.com>
Co-authored-by: CitrusIce <31264099+CitrusIce@users.noreply.github.com>
Co-authored-by: minisforum <no@mail.com>
Co-authored-by: Ravi Tharuma <25951435+RaviTharuma@users.noreply.github.com>
Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Prudhvi Vuda <53619858+Prudhvivuda@users.noreply.github.com>
Co-authored-by: Ridho Pratama <p.ridho9@gmail.com>
Co-authored-by: Bob.Hou <houminxi@gmail.com>
Co-authored-by: Ravi Tharuma <noreply@github.com>
Co-authored-by: Markus Hartung <markus.hartream@gmail.com>
2026-07-25 02:51:07 -03:00
Jay Ongg
58ab8b1d2c Clicking a provider card hitting back loses scroll position (#8349)
* clicking a provider card hitting back loses scroll position

* clean up code

* Rename some funcitons

* minor UX updates

* add null-guard in highlight()

* Add providerCardHandle tests

* add highlight tests. refactored code into separate utility functions

* Minor change to skip a firstElementChild call - use a ref to access the Link inside ProviderCard.
2026-07-25 02:51:00 -03:00
Adrian Rogala
9a0e764459 feat(cli): replace ANTHROPIC_SMALL_FAST_MODEL with Fable default (#8343)
Claude Code retired ANTHROPIC_SMALL_FAST_MODEL; expose
ANTHROPIC_DEFAULT_FABLE_MODEL from the claude registry instead.
2026-07-25 02:50:53 -03:00
Diego Rodrigues de Sa e Souza
53a91b3df8 feat(api): quota-aware fallback routing for web-fetch providers (#8297) (#8335)
Mirror the search/route.ts pattern for /v1/web/fetch: skip rate-limited
stubs instead of letting them short-circuit auto-select, walk the
fixed-priority pool (fill-first) with a request-time fallback on
retryable/quota upstream statuses (429 always; 402/403 for
Firecrawl/Tavily/TinyFish quota-style tiers), and return a proper 429
(with Retry-After) when the whole pool is exhausted instead of a
generic 400. Explicit-provider requests never silently fall back.
2026-07-25 02:50:46 -03:00
Diego Rodrigues de Sa e Souza
b8901b6506 feat(db): persist caller session tag into call_logs for per-session cost attribution (#8249) (#8334) 2026-07-25 02:50:40 -03:00
Diego Rodrigues de Sa e Souza
1cafd328c7 fix(dashboard): persist compression engine detail settings (Headroom / session dedup / CCR) instead of dropping them on save (#8388) (#8404) 2026-07-24 20:37:37 -03:00
Diego Rodrigues de Sa e Souza
09e9ecef97 fix(resilience): treat an unreachable-proxy ECONNREFUSED as a circuit-breaker event so combo fails over instead of hitting the 503 max-retry limit (#8376) (#8403) 2026-07-24 20:37:31 -03:00
Diego Rodrigues de Sa e Souza
1f58a29e9c fix(api): estimate inline base64 image tokens instead of counting the data URL as text so it does not falsely exceed the context window (#8368) (#8401) 2026-07-24 20:37:25 -03:00
Diego Rodrigues de Sa e Souza
73762b1b32 fix(backend): stop prompt-cache affinity from silently reordering an explicit priority combo across models (#8370) (#8400) 2026-07-24 20:37:18 -03:00
Diego Rodrigues de Sa e Souza
544ae2d3da fix(api): accept a missing status query-param on GET /api/plugins instead of rejecting null with Invalid status value (#8374) (#8399) 2026-07-24 20:37:12 -03:00
Diego Rodrigues de Sa e Souza
312e24e785 fix(plugins): fire registered+active plugin hooks (onRequest/onResponse/onError) during proxying instead of never invoking them (#8395) (#8449)
Co-authored-by: ikelvingo <im.kelvinwong@gmail.com>
2026-07-24 20:36:59 -03:00
Diego Rodrigues de Sa e Souza
d7f9475864 fix(backend): make disabling the global per-key proxy toggle override existing per-key proxy assignments (#8385) (#8447)
Co-authored-by: ikelvingo <im.kelvinwong@gmail.com>
2026-07-24 20:36:52 -03:00
Diego Rodrigues de Sa e Souza
b64361dd2c fix(resilience): cap the connection cooldown after a 429 burst so combo fallback is not blacked out past the real rate-limit window (#8396) (#8446)
Co-authored-by: ikelvingo <im.kelvinwong@gmail.com>
2026-07-24 20:36:44 -03:00
Diego Rodrigues de Sa e Souza
9a78ea2225 fix(providers): stop marking a multi-quota-window provider exhausted when only some windows are depleted (LIMIT-200 snapshot eviction drops idle healthy windows) (#8431) (#8445)
Co-authored-by: ikelvingo <im.kelvinwong@gmail.com>
2026-07-24 20:36:37 -03:00
diegosouzapw
36f8fd1005 docs: enhance README with tables for improved structure and readability 2026-07-24 13:07:06 -03:00
diegosouzapw
0f226a5a24 docs: enhance README formatting with tables for better structure and readability 2026-07-24 12:33:56 -03:00
Diego Rodrigues de Sa e Souza
e8719783ef fix(api): stop the 2000-token safety buffer from inflating usage.prompt_tokens in the client response (#8331) (#8356)
* fix(api): stop the 2000-token safety buffer from inflating usage.prompt_tokens in the client response (#8331)

* fix(sse): scope #8331's usage-buffer fix around Claude-Code-compatible providers

The #8331 fix correctly stopped folding the 2000-token context-window safety
margin into client-visible prompt_tokens/input_tokens/total_tokens for normal
API metering clients. But it also silently changed the response shape for
Claude-Code-compatible providers, whose own context accounting reads the
buffered number straight out of usage — regressing
tests/unit/cc-compatible-provider.test.ts (expected 2007, got 7).

Fold the computed context_budget_* fields back into the visible usage fields
for that one path only (applyClientUsageBuffer's new
preserveContextBudgetInVisibleUsage option, gated on the existing
isClaudeCodeCompatible flag in chatCore.ts). Every other caller keeps the
real, unbuffered #8331 numbers.
2026-07-24 12:10:06 -03:00
Diego Rodrigues de Sa e Souza
9994e00763 refactor(sse): classify SSE critical-path empty catches + add CONTRIBUTING convention (#8142) (#8364) 2026-07-24 11:45:10 -03:00
Diego Rodrigues de Sa e Souza
9b7bd6e5d3 fix(api): stop leaking the internal provider UUID in /v1/models and honor the configured prefix (#8327) (#8361) 2026-07-24 11:45:02 -03:00
Diego Rodrigues de Sa e Souza
cbc7786533 fix(backend): keep combo routing from dispatching image requests to text-only targets (#8332) (#8360) 2026-07-24 11:44:54 -03:00
Diego Rodrigues de Sa e Souza
fa46d93941 fix(api): accept the current compatible-provider connection id scheme in the models test route (#8326) (#8359) 2026-07-24 11:44:46 -03:00
Diego Rodrigues de Sa e Souza
493708f575 fix(sse): strip third-party-agent signals from the Hermes system prompt that trigger Anthropic 400 extra-usage (#8350) (#8358) 2026-07-24 11:44:37 -03:00
Diego Rodrigues de Sa e Souza
70275be59b fix(dashboard): show custom provider_nodes providers in the Topology view instead of only AI_PROVIDERS (#8328) (#8357) 2026-07-24 11:44:29 -03:00
Markus Hartung
4323dba518 fix(sse): stop stripInternalReasoningPlaceholder from eating inter-word spaces (#8341)
Live incident: streamed assistant text was losing the spaces BETWEEN words
(e.g. "Bilden är en riktig JPEG nu" -> "Bildenärenriktig JPEG nu") on the
Responses-API and Claude streaming paths.

stripInternalReasoningPlaceholder() (#8081/#8162) is called on every
individual delta.content chunk, and unconditionally called .trim() even when
its sentinel ("(prior reasoning summary unavailable)") was never present in
that chunk. Tokenizers commonly emit sub-word tokens with a leading space as
part of the token (e.g. " en", " riktig") -- each such chunk got its only
whitespace character (the inter-word space) silently trimmed away before
being appended to the accumulated message, while the words themselves stayed
intact. Punctuation-only chunks were largely unaffected, matching what was
observed live.

Only trims when the sentinel is actually present -- preserves the original
#8081 intent (collapse a placeholder-only chunk to "") without touching the
overwhelming majority of chunks that never contain it.

Co-authored-by: Markus Hartung <markus.hartream@gmail.com>
2026-07-24 11:44:21 -03:00
Markus Hartung
dec4a67fe7 fix(memory): self-heal upsertVector/deleteVector from a raced vec_memories drop (#8337)
Live incident: memory.vec.upsert.fail {"error":"no such table: vec_memories"}
recurred repeatedly right after restarts, even though ensureReady() is called
immediately beforehand. ensureReady()'s signature-check-then-maybe-recreate
logic (resetForSignature does DROP TABLE IF EXISTS + CREATE VIRTUAL TABLE) is
not synchronized against a concurrent caller's upsertVector/deleteVector -- a
second in-flight memory write that independently decides (from a stale read
of memory_vec_meta) it also needs to reset the table can drop it out from
under another write's insert. Confirmed live: memory_vec_meta showed
vec_loaded=0 for the entire session across many restarts, then flipped to 1
mid-investigation once one attempt finally completed without interruption --
consistent with an intermittent race, not a permanently broken path (verified
the underlying sqlite-vec extension and CREATE VIRTUAL TABLE statement work
correctly in isolation, both on the host and inside the production container).

Rather than chase the exact interleaving (every underlying SQLite call is
synchronous via better-sqlite3, so the race window is narrow and did not
reproduce under simple Promise.all stress tests), makes the write path
resilient to arriving after the table was dropped: on a "no such table"
error, recreate vec_memories from the last-known-good memory_vec_meta
dimension and retry once.

Co-authored-by: Markus Hartung <markus.hartream@gmail.com>
2026-07-24 11:44:09 -03:00
Ravi Tharuma
07067841b5 feat(combo): enforce provider and model family invariants (#8304)
* feat(combo): enforce provider and model family invariants

Closes #8279

Co-Authored-By: Ravi Tharuma <ravitharuma@users.noreply.github.com>

* fix(combo): complete invariant enforcement paths

Map invariant failures to structured API errors, correct target diagnostics, and validate restored combos inside the existing migration transaction.

Co-Authored-By: Ravi Tharuma <noreply@github.com>

---------

Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>
Co-authored-by: Ravi Tharuma <noreply@github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-24 11:44:00 -03:00
Bob.Hou
97d948cc9e fix(antigravity): add missing gemini-3.6-flash pricing rows to ag OAuth pricing (#8290)
release/v3.8.49 already ships the Gemini 3.6 Flash catalog entries
(AGY_PUBLIC_MODELS, ANTIGRAVITY_PUBLIC_MODELS, MODEL_SPECS with
supportsThinking: false — Antigravity still rejects client-supplied
thinking params) via #8013. What was still missing: the `ag` pricing
rows in DEFAULT_PRICING_OAUTH, so getPricingForModel("ag", id)
returned null for the three tiers and cost/quota calculations
silently fell back to $0.

Pricing: $1.50 input / $7.50 output / $0.15 cached per MTok (Google's
2026-07-21 announcement), matching the existing 3.5-flash schedule
shape. Thinking tokens billed at output rate.

Extends the existing pricing-ag-flash-tiers.test.ts (RED-first: all
three tiers failed the "non-null pricing row" assertion before this
change) rather than re-adding the already-shipped catalog/modelSpecs
entries.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-24 11:43:52 -03:00
Diego Rodrigues de Sa e Souza
2e355dd0b9 chore(deps): bump next to 16.2.11 (9 security advisories) (#8265)
Closes 9 Dependabot alerts (#135-#143) — Next.js 16.0.0..<16.2.11:
SSRF in Server Actions/rewrites, cache confusion, DoS (Server Actions,
Image Optimization SVG, Edge payload), middleware/proxy bypass, and
unauthenticated Server Function endpoint disclosure.

Lockfile bump within the existing ^16.2.6 range (now floored at
^16.2.11); no production code touched.

Co-authored-by: rafaumeu <rafael.zendron22@gmail.com>
2026-07-24 11:43:44 -03:00
Ravi Tharuma
4e85e3d920 feat(opencode-plugin): auto-discover models while running + force sync (#8101)
* feat(opencode-plugin): auto-discover models while running + force sync

Add Pi-parity discovery for OpenCode:
- autoSyncIntervalMs background refresh (default 5m, min 60s, 0=off)
- omniroute_sync_models tool to force cache invalidate + /v1/models refetch
- /omni-sync and /omni-autosync command templates (OpenCode has no slash API)

* docs(opencode-plugin): document auto model discovery + /omni-sync

Document Pi-parity catalog refresh for OpenCode:
- autoSyncIntervalMs background discovery (default 5m)
- omniroute_sync_models force-refresh tool
- /omni-sync and /omni-autosync command templates

---------

Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>
2026-07-24 11:43:35 -03:00
diegosouzapw
44eb05469a docs: improve formatting and structure in README for better readability 2026-07-24 11:29:18 -03:00
diegosouzapw
4f9cd0c92a docs: update section headers in README for improved clarity 2026-07-24 10:28:21 -03:00
diegosouzapw
cad4ea92ff docs: update provider icons and enhance API interface documentation 2026-07-24 10:19:17 -03:00
Prudhvi Vuda
af60f41e99 fix(providers): reconcile Kimi K3 vision when attachment contradicts modalities (#8250) (#8313)
* fix(providers): reconcile Kimi K3 vision when attachment contradicts modalities

Synced models.dev rows for kimi-coding*/k3 can ship attachment=false while
modalities_input still lists image/video. Prefer the modality signal (and
normalize at sync + resolve) so supportsVision, attachment, and exposed
modalities agree.

Closes #8250

* fix(providers): keep Kimi K3 static fallback text-only (#8250)

The Kimi K3 vision reconciliation added supportsVision=true directly
to the kimi-coding registry entry for id "k3". That entry is the
static/stable fallback catalog used when discovered capabilities are
unavailable, and it must stay text-only per #4071 — the vision fix is
already applied correctly on the discovered path via
MODEL_SPECS["kimi-k3"] (aliases: ["k3"]) and
modelCapabilities.ts::resolveVisionCapability.

Restores the invariants guarded by
tests/unit/kimi-k2.7-code-registration.test.ts ("Kimi Code k3 fallback
leaves discovered capabilities unset") and
tests/unit/catalog-updates-v3829-kimi-qwen.test.ts ("kmca stable
fallback only carries documented static capabilities").

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-24 10:18:21 -03:00
Bob.Hou
406f41de30 fix(translator): cap thinking budget on explicit budget_tokens path (#8312)
* fix(translator): cap thinking budget on explicit budget_tokens path

* fix(translator): stop dropping thinkingConfig on cap-0 reasoning_effort path

The thinking-budget-cap guard added in this branch skipped thinkingConfig
entirely whenever a model's thinkingBudgetCap was 0 (e.g. gemini-3-flash),
including on the reasoning_effort/budgetMap path. That regressed the
pre-#6943 native-defaults contract (thinkingBudget 0 / includeThoughts
false must still be present) and crashed callers that read
`.thinkingConfig.thinkingBudget` unconditionally
(translator-openai-to-gemini-defaults.test.ts).

Also restore includeThoughts:true on the Claude-format explicit
thinking.budget_tokens path (openai-to-gemini.ts's Claude-format field and
claude-to-gemini.ts's native thinking field): budget_tokens:0 there is the
client's dynamic-thinking sentinel (#6813), not an off-switch, and must
stay true even after the new capping — the cap must only clamp positive
explicit values, never flip the zero sentinel's semantics.

Updates two tests this branch added that encoded the incorrect
"omit thinkingConfig / includeThoughts:false for the 0 sentinel" behavior,
to match the pre-existing, still-required contracts above.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(providers): revert scope-creep flip of Gemini 3.5/3.6 Flash supportsThinking

The thinking-budget-cap fix accidentally expanded 5 shorthand modelSpecs
entries (gemini-3.5-flash, gemini-3.5-flash-low, gemini-3.6-flash-high/
medium/low) into explicit objects setting supportsThinking:true and
thinkingBudgetCap:24576. That flip was unrelated to the two proven test
regressions (translator-openai-to-gemini-defaults.test.ts and
claude-to-gemini-budget-tokens-zero-6813.test.ts, which only exercise
gemini-3-flash-preview, gemini-3.1-pro and gemini-2.5-pro) and reopens a
deliberately closed path from #8013: Antigravity still rejects
client-supplied thinking params for these Gemini 3.5/3.6 Flash tier ids,
so supportsThinking must stay false (inherited from
GEMINI_35_FLASH_MODEL_SPEC).

Reverted all 5 entries back to the release shorthand
`{ ...GEMINI_35_FLASH_MODEL_SPEC }`. gemini-3-flash, gemini-3.1-pro and
gemini-2.5-pro (the models the regression tests actually exercise) were
already correctly specced in the release baseline and are untouched.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-24 10:18:12 -03:00
Diego Rodrigues de Sa e Souza
286574cf39 docs: one golden path across PR template, CONTRIBUTING, GEMINI, AGENTS + CI milestones in ROADMAP (#8380)
Contributor guidance contradicted itself in four places (found in the #8084 review):

- pull_request_template.md + CONTRIBUTING.md asked contributors to run the FULL
  unit suite + coverage gate locally, while the maintainer's stated golden path
  (#8273/#8329) is: focused tests for the change locally; full suite, coverage,
  and build are CI's job. On 16GB hosts the full local chain has saturated
  machines (#8084 incident report).
- GEMINI.md demanded coverage >= 75/75/75/70 while the official CI gate is
  60/60/60/60 (quality-baseline ratchet on top).
- AGENTS.md fork workflow said to branch from upstream/main; the default branch
  is the active release/vX.Y.Z line (main only receives release squash-merges).

Also makes the #8084 CI direction explicit in the public ROADMAP: lane
consolidation (3.8.51), one CI policy for release/** and main (3.8.52),
full-regression authority -> merge queue after TIA shadow evidence (3.8.54),
preview-artifact + build-once rehearsal inside the 3.8.58 dry-run.

Refs #8329
Refs #8084
2026-07-24 10:00:19 -03:00
Diego Rodrigues de Sa e Souza
7c3b987ee3 chore(ci): cancel superseded runs, skip DAST on docs-only PRs, persist TIA shadow evidence (#8379)
Runner-cost pass grounded in the #8084 review of the current pipeline:

- dast-smoke.yml: add concurrency cancel-in-progress (25-min advisory builds were
  stacking on force-push storms) and paths-ignore for docs/**+**/*.md — a docs-only
  PR cannot change DAST behavior but was paying the 6-11min CLI-bundle build.
- semgrep.yml: add concurrency cancel-in-progress. No paths filter on purpose:
  p/secrets must keep scanning docs-only diffs (credentials leak in .md too).
- quality.yml (TIA step): persist the per-PR impacted-test selection to a
  tia-selection artifact + GITHUB_STEP_SUMMARY line. This is the shadow-evidence
  phase: TIA false negatives become measurable against fast-unit's full-suite
  verdict across releases BEFORE any gate authority moves off ordinary PRs.

Refs #8084
2026-07-24 10:00:16 -03:00
Diego Rodrigues de Sa e Souza
1e0886cd09 test: hermetic notion thread-session mocks + drop duplicated usage-analytics suite (#8392)
* test: hermetic TLS mock for notion thread-session suite + drop duplicated usage-analytics file

Root cause A (#8159): sendNotionInferenceRequest() in
open-sse/executors/notion-web.ts was migrated from fetch() to
tlsFetchNotion() (open-sse/services/notionTlsClient.ts, native
tls-client-node binary) to get past Notion's Cloudflare TLS
fingerprinting. #8159 updated the mock in the sibling
tests/unit/executor-notion-web.test.ts (installNotionTlsMock, wired
through __setTlsFetchOverrideForTesting) but never touched
tests/unit/executor-notion-web-thread-sessions.test.ts (split out
earlier by #7900) — its 3 execute()-driven tests still mocked
globalThis.fetch, which tlsFetchNotion() never calls once the native
TLS client loads successfully. Confirmed live: all 3 tests hit real
https://app.notion.com with a fake cookie and got a real 401
(~8.1-8.5s each here; on a network with blocked/slow egress this
would instead hang up to the client's ~190s timeout+grace per test —
a CI-hang risk).

Fix: replicate installNotionTlsMock verbatim from the sibling file
into executor-notion-web-thread-sessions.test.ts so the 3 tests mock
the TLS override point instead of global fetch. Suite is now fully
hermetic — 8/8 pass, no network I/O, total runtime 31.0s -> 8.0s.

Root cause B (#7700): tests/unit/usage-analytics-route-extra.test.ts
was created as a byte-for-byte duplicate of 10 of the 22 tests in
tests/unit/usage-analytics-route.test.ts. #7300 later fixed a fixture
bug in the retention-window boundary test ("does not double-count raw
and aggregated rows") in the main file only — reading
getUserDatabaseSettings().retention.usageHistory live instead of a
hardcoded 30-day cutoff (default retention is 365 days) — leaving the
duplicate copy on the stale hardcoded value, which now fails (1 !== 2).

Fix: delete the duplicate file. All 10 of its test names exist
verbatim in the main file (verified with comm -12) and that file
passes 22/22:
  - does not double-count raw and aggregated rows
  - does not persist guessed API key attribution
  - does not throw Unknown named parameter on short range (needsAggregated=false)
  - does not throw Unknown named parameter with apiKey filter on long range
  - groups renamed API key usage by stable ID
  - includes activityMap for heatmap
  - includes cost by API key
  - omits global aggregates when filtering by API key
  - returns 500 on database errors
  - returns weeklyPattern for the costs dashboard
No coverage loss — same production code, same assertions, one fewer
redundant file.

Validation:
  - RED executor-notion-web-thread-sessions.test.ts: 5 pass / 3 fail
    (401 !== 200, real network hit), 31.0s
  - RED usage-analytics-route-extra.test.ts: 9 pass / 1 fail
    (1 !== 2), 18.7s
  - GREEN executor-notion-web-thread-sessions.test.ts: 8/8 pass, 8.0s,
    hermetic (no network)
  - GREEN executor-notion-web.test.ts (sibling, untouched): 37/37
    pass, byte-identical diff
  - GREEN usage-analytics-route.test.ts (untouched): 22/22 pass,
    byte-identical diff
  - npx eslint on the changed file: clean
  - npm run typecheck:core: clean (exit 0)

Refs #8159
Refs #7300
Refs #7700

* chore(quality): register usage-analytics-route-extra deletion in test-masking allowlist

check:test-masking hard-flags any deleted test file without a
_deletedWithReplacement entry. The deletion is legitimate (100% duplicate
suite, coverage retained verbatim in tests/unit/usage-analytics-route.test.ts)
-- same registration pattern as the video-dashscope entry.

Refs #7700
Refs #7300
2026-07-24 10:00:07 -03:00
Diego Rodrigues de Sa e Souza
fbea867d11 test: realign catalog snapshot tests to current deliberate catalog state (#8386)
* test: realign catalog snapshot tests to current deliberate catalog state

Six catalog/snapshot tests drifted behind deliberate catalog changes that
were already validated by newer sibling tests. No production code touched;
every change aligns a stale snapshot to behavior already validated by newer
sibling tests.

Root causes (all confirmed against the current code before editing):

- tests/unit/providers-constants-split.test.ts: APIKEY_PROVIDERS grew from
  187 to 195 entries via #8077 (clova-studio/internlm/ant-ling, regional),
  #8161 (sarvam/plamo → regional, writer → frontier-labs) and #8170
  (typhoon → regional, inception → frontier-labs). Family counts verified
  to sum to 195 (gateways 60, frontier-labs 24, inference-hosts 28,
  enterprise-cloud 17, regional 40, specialty-media 26) with no duplicates.
  Updated the two assertions and extended the changelog comment.

- tests/unit/qianfan-provider.test.ts: the expected Baidu Qianfan website
  URL was the pre-#8128 wenxinworkshop path. #8128/#6271 moved it to
  https://cloud.baidu.com/product-s/qianfan_home, already locked by the
  sibling regression test tests/unit/baidu-qianfan-website-urls-6271.test.ts.

- tests/unit/t31-t33-t34-t38-model-specs.test.ts and
  tests/unit/auto-combo-credentialed-model-pool.test.ts: the Antigravity
  catalog refactor (#8013) retired gemini-3-pro-preview/claude-sonnet-5 and
  renamed the Gemini 3.5 Flash tiers (low/medium/high ->
  extra-low/low/gemini-3-flash-agent), confirmed against
  ANTIGRAVITY_PUBLIC_MODELS and tests/unit/antigravity-retired-public-models.test.ts.
  Swapped the retired IDs for currently-registered ones
  (gemini-3.6-flash-high, claude-sonnet-4-6, gemini-3-flash-agent,
  gemini-3.5-flash-low/extra-low) and moved the wildcard-exclusion prefix
  test from the now-2-tier "gemini-3.5-*" group to "gemini-3.6-*", which has
  3 real tiers today (same >=3 semantics, just pointed at a prefix that
  still has 3 members).

- tests/unit/model-alias-seed.test.ts: getModelInfo("gemini-3.1-pro") now
  canonicalizes through ALIAS_TO_PROVIDER_ID["agy"] = "antigravity" (#8050),
  the same pattern already applied to opencode -> opencode-zen. Updated the
  expected provider id.

- tests/unit/video-dashscope.test.ts (deleted, 216 lines): #8266
  reorganized the Alibaba video catalog so the flat wan2.7-t2v id no longer
  exists under the plain "alibaba" provider (only the dated
  wan2.7-t2v-2026-06-12 does); the flat id now lives only under
  "qwen-cloud". All 6 tests in the file failed because they built  requests
  against alibaba/wan2.7-t2v, which the new allowlist now rejects with 400
  ("unsupported alibaba video model") - verified directly against
  VIDEO_PROVIDERS in open-sse/config/videoRegistry.ts. Coverage already
  exists and was confirmed passing pre-deletion in
  tests/unit/alibaba-video-media.test.ts (including an explicit
  "Alibaba rejects video models outside its own allowlist" case for this
  exact id) and tests/unit/qwen-cloud-video-media.test.ts (covers the same
  id under qwen-cloud). Note: the deleted file's DashScope upstream
  error-path assertions (401 missing credentials, 502 missing task_id, 502
  FAILED status, 504 poll timeout) don't have a byte-for-byte equivalent in
  the two replacement files, though the shared dashscopeHandler.ts code
  path they exercise remains covered by several sibling *-media.test.ts
  files for the happy path and local validation.

- tests/unit/authz/spawn-capable-prefixes-client-safe.test.ts: #7892 added
  /api/vnc-session to the SPAWN_CAPABLE_PREFIXES deny-list (Hard Rules
  #15/#17 hardening). Bumped the expected length 10 -> 11 and added the
  entry to the test's named list for documentation.

Refs #8013, #8050, #8266, #7892, #8128

* chore(quality): allowlist the video-dashscope.test.ts deletion with its replacements

check:test-masking (pr-test-policy CI gate) requires a _deletedWithReplacement
entry for any deleted test file, even when the deletion is a verified-legitimate
supersession. Documents the same #8266 rationale from the prior commit in the
machine-checked allowlist so the deletion is not flagged as unexplained masking.

Refs #8266
2026-07-24 09:59:08 -03:00
Diego Rodrigues de Sa e Souza
875de01de7 chore(quality): drop stale muse-spark-web allowlist entry + sync sidebar order snapshots (#8383)
Two independent "code is right, bookkeeping lagged" base-reds:

1. #8233 made open-sse/executors/muse-spark-web.ts import
   sanitizeErrorMessage from utils/error.ts (a real Rule #12 fix), but
   left its KNOWN_MISSING_ERROR_HELPER allowlist entry in
   scripts/check/check-error-helper.mjs in place. The gate's own
   stale-allowlist enforcement (assertNoStale) correctly flagged the now
   -obsolete entry: `npm run check:error-helper` failed with "1 entrada(s)
   obsoleta(s)", and tests/unit/check-error-helper.test.ts's "the shipped
   allowlist freezes exactly the known current violators" test expected
   an empty Set. Removed the entry (kept the assertNoStale machinery and
   the general scope-header comments untouched).

2. #8064 added the "compression-exclusions" sidebar item right after
   "compression-studio" in COMPRESSION_CONTEXT_GROUP (deliberate,
   complete feature) but didn't update two order-snapshot tests written
   before that item existed:
   - tests/unit/sidebar-visibility.test.ts expected the "omni-proxy"
     section's flattened id list to end the compression block at
     "compression-studio".
   - tests/unit/ui/sidebar-engine-items.test.ts asserted "Studio must be
     last" in COMPRESSION_CONTEXT_GROUP.
   Updated both to the real, intentional order: Settings -> Combos ->
   engines -> Studio -> Exclusions (Studio now second-to-last,
   Exclusions last).

Validation (red -> green):
- check:error-helper gate: red ("1 entrada(s) obsoleta(s)") -> green
  ("OK (898 files scanned, 0 known-missing frozen)")
- tests/unit/check-error-helper.test.ts: 31/32 -> 32/32
- tests/unit/sidebar-visibility.test.ts: 6/7 -> 7/7
- tests/unit/ui/sidebar-engine-items.test.ts: 13/14 -> 14/14

Refs #8233
Refs #8064
2026-07-24 09:58:58 -03:00
Diego Rodrigues de Sa e Souza
afe3a931f9 fix(i18n): restore #8219 CacheSettingsTab key sync + synthetic fixture for zh-TW repro test (#8387)
Root cause (two independent causes):
1. PR #8219 (commit 2a865aaaa7) added CacheSettingsTab.tsx with 12
   t("settings.*") calls whose keys were never created anywhere, not even
   in en.json (the source of truth). The same PR added only 3 sidebar/header
   keys (settingsCache, settingsCacheSubtitle, settingsCacheDescription) to
   en+es, without running `npm run i18n:sync-ui` to propagate to the other
   41 locales.
2. tests/unit/i18n-missing-placeholder-fallback.test.ts had a "#7258 repro"
   test asserting the real zh-TW.json still carried raw __MISSING__:
   placeholders — a premise invalidated by #8024, which completed the
   Traditional Chinese translation to 100%.

What changed:
- Added the 12 missing settings.* keys to en.json, mirroring the sibling
  requestBodyLimit* family (placeholders {min}/{max}/{value} match the
  component exactly).
- Added real, natural translations for all 15 CacheSettingsTab-related keys
  (12 settings.* + 3 sidebar/header) to pt-BR.json, vi.json and es.json (es
  already had the 3 sidebar/header keys).
- Ran `npm run i18n:sync-ui` (official tool, no locale hand-edited) to stub
  the remaining 39 locales with __MISSING__:<english>. This also discovered
  17 pre-existing unrelated missing keys (compression-exclusions settings,
  8 new-provider onboarding descriptions) never synced since #8031, and
  pruned 3 dead orphaned zh-TW-only keys (codexSessionAffinity{Title,Desc,
  Ttl}, superseded by the generic sessionAffinity* keys since #7274,
  confirmed unused anywhere in src/) — verified programmatically as
  +32/-0/~0 changed per stub locale, +32/-3/~0 changed for zh-TW.
- Rewrote the "#7258 repro" test to use a synthetic fixture (same style as
  the sibling deepMergeFallback fixtures in the same file) instead of
  depending on zh-TW.json's real, evolving translation-completeness state.
  Proves the same behavior: collectPlaceholderLeaves() detects a raw
  __MISSING__: leaf before deepMergeFallback (the fix) is exercised.

Validation: all 4 previously-red files green (23/23 assertions). Broader
sweep of 271 i18n-adjacent unit tests unaffected. i18n:check-ui-coverage
(42/42 locales >=80%, 99.7-100%) and i18n:check-glossary both pass.
typecheck:core clean.

Refs #8219
Refs #8024
2026-07-24 09:57:15 -03:00
Diego Rodrigues de Sa e Souza
0ba68ce482 fix(compression): rank codex-responses in adaptive-ladder maps (#8381)
#8010 registered the codex-responses engine in the compression catalog
(engineCatalog.ts, stackPriority 12 between rtk's 10 and headroom's 15)
but never added it to adaptiveCompression/ladder.ts's AGGRESSIVENESS and
REDUCTION_FACTOR maps. Those maps' own header documents that they must
cover every real catalog/registry engine, not just the 7 in
DEFAULT_LADDER, so an operator adding codex-responses via ladderOverride
silently fell back to aggressivenessOf() === 0 (same as "off") and
expectedReductionFactor() === 0.9 (the generic default), breaking
floor-mode escalation ranking for any ladder that includes it.

Add "codex-responses" to both maps between rtk and ionizer, matching
its stackPriority (12) sitting between rtk's (10) and ionizer's (13):
- AGGRESSIVENESS: 22 (between rtk's 20 and ionizer's 25)
- REDUCTION_FACTOR: 0.84 (between rtk's 0.85 and ionizer's 0.83),
  reflecting its "lossless-first, bounded diagnostic" guidance in
  engineCatalog.ts

Validation: tests/unit/ladder-engine-maps-6533.test.ts red -> green
(2 of 3 tests were failing on the missing engine; all 3 pass after the
fix). Sanity-checked neighbors compression-exclusions.test.ts and
compression/adaptive-resolve-plan.test.ts still pass.

Refs #8010
2026-07-24 09:57:12 -03:00
Diego Rodrigues de Sa e Souza
bb4cb86be2 fix(sse): family auto-combos include any backend that serves the family (no-auth allowlist scoped to tier pools) (#8391)
Context: #8183 introduced AUTO_COMBO_NOAUTH_ALLOWLIST (opencode, felo-web) to
gate no-auth providers out of every auto/* candidate pool, motivated by public
HTTP egress reliability on the reference VPS (.15) — several no-auth backends
(duckduckgo-web, theoldllm, chipotle, aihorde) were flaky there. Its own tests
(noauth-autocombo-allowlist.test.ts, virtual-auto-combo.test.ts) never
exercised the auto/<family> path, so the gate silently applied there too.

auto/<family> combos (#6453, e.g. auto/glm, auto/zai) are a different axis:
an identity selector ("route to whatever genuinely serves GLM"), not a
reliability-curated pool. auggie (local CLI subprocess, zero HTTP egress —
the reliability concern #8183 targets doesn't even apply to it) advertises a
literal glm-5.2 model and had an explicit design-test seat in auto/glm since
#7032, but the #8183 allowlist silently excluded it from that pool.

Operator decision (2026-07-24): the no-auth allowlist gate keeps applying to
category/tier and flat-variant auto/* pools (auto/best-free, auto/coding:fast,
...), but auto/<family> pools bypass it — any no-auth backend that genuinely
serves the family is admitted.

Fix: thread a `bypassAllowlist` flag through isChatAutoComboNoAuthProvider()
and getNoAuthCandidates(), set to `Boolean(spec?.family)` at the single call
site in createVirtualAutoCombo(). Family narrowing (buildFamilyCandidateFilter)
still runs afterward, so a bypassed no-auth candidate only survives if its
model actually belongs to the requested family. Category/tier and flat-variant
pools (spec.family unset) keep the gate fully intact.

Validation:
- tests/unit/autoCombo/provider-family-combos.test.ts:136 was red (expected
  ["auggie","glm","zai"], got ["glm","zai"]) — now green (11/11 passing).
- tests/unit/noauth-autocombo-allowlist.test.ts (3/3) and
  tests/unit/virtual-auto-combo.test.ts (10/10, including "restricts the
  no-auth pool to the allowlist") stay green — the #8183 gate is untouched for
  spec-less/category/tier pools.
- Full tests/unit/autoCombo/ vitest sweep: 5 files, 36/36 passing.
- npm run typecheck:core clean.
- 4 unrelated failures pre-exist on origin/release/v3.8.49 (verified via `git
  show HEAD:<path>` swap, no stash) in
  tests/unit/auto-combo-credentialed-model-pool.test.ts (antigravity/gemini-3.5
  credentialed-pool logic, untouched by this change).

Refs #8183, Refs #6453, Refs #7032
2026-07-24 09:57:08 -03:00
Diego Rodrigues de Sa e Souza
c5c27b813a fix(sse): cap exact cooldowns only when synthetic — verified upstream resets pass uncapped (#8393)
Contract vs cap: #6863 requires a model lockout to honor a VERIFIED
upstream quota reset exactly (e.g. Antigravity "Resets in 92h27m28s",
shipped in v3.8.47). #7940 requires SYNTHETIC exact-cooldown estimates
(the quota_exhausted until-midnight heuristic) to respect the
operator's maxCooldownMs so they cannot balloon unbounded. Both are
legitimate, non-conflicting contracts — they apply to different kinds
of values.

Root cause: #7980 (fixing #7940) changed recordModelLockoutFailure()
in open-sse/services/accountFallback.ts to unconditionally clamp
every exactCooldownMs against maxCooldownMs, with no way to
distinguish a verified upstream reset from a synthetic estimate. A
real ~92h reset got clamped to the operator's ~30min cap, and the
router went on hammering 429 against quota that was known not to
recover for days — regressing #6863's contract by omission, not by
new policy (the "honor it exactly" docstrings on
selectLockoutCooldownMs() and its call sites were left untouched and
now describe dead code).

Fix: add an opt-in `exactCooldownVerified` flag to
recordModelLockoutFailure()'s options. When true, exactCooldownMs
bypasses the maxCooldownMs clamp entirely; when false/omitted
(the default), behavior is byte-identical to before this change.
Set the flag only at the 4 call sites that already carry upstream
provenance for the value they pass — usedUpstreamRetryHint /
quotaResetHintMs from checkFallbackError():
  - open-sse/services/combo.ts (2 sites): exactCooldownVerified
    mirrors lockoutHintMs > 0, which is only ever nonzero when it
    traces back to a genuine upstream signal.
  - src/sse/services/auth.ts (2 sites): exactCooldownVerified mirrors
    the same usedUpstreamRetryHint / quotaResetHintMs check already
    used to derive exactCooldownMs at each site.
The quota_exhausted → until-midnight synthetic default and plain
exponential backoff are untouched and stay capped, per #7940. The
two other recordModelLockoutFailure call sites (combo.ts quality
failure, auth.ts local-404/grok-web-403) never carry a verified hint
and were left unmodified.

Validation (TDD): tests/unit/combo-lockout-quota-reset-6863.test.ts
red→green with its assertions unchanged (was clamping ~332,848,000ms
to ~1,799,995ms; now honors the parsed reset). Added a boundary pair
to tests/unit/model-lockout-exact-cooldown-cap.test.ts proving the
same magnitude resolves differently by provenance: synthetic stays
capped, verified passes through whole. Full existing suite in that
file plus combo-model-lockout-honors-reset-1308.test.ts stay green
unmodified. Swept 45 lockout/cooldown-adjacent test files (502/505
passing); the 3 failures reproduce byte-identical on a pristine
origin/release/v3.8.49 checkout (PROVIDER_BREAKER_FAILURE_STATUSES
ReferenceError in untouched chat.ts, and a documented timing-sensitive
serial test) — confirmed pre-existing, out of this fix's scope.
npm run typecheck:core and npm run lint are clean.

Refs #6863
Refs #7940
Refs #7980
2026-07-24 09:57:01 -03:00
Diego Rodrigues de Sa e Souza
f0096f0224 fix(resilience): terminal-skip spares the recoverable GitHub Copilot no_refresh_token state (#8389)
Cause: #8182's terminal-connection guard in checkConnection() returns
early for any testStatus in {credits_exhausted, banned, expired} to
stop the sweep from wasting CPU/network probing connections that can
never self-heal. But testStatus="expired" + errorCode="no_refresh_token"
is exactly the state the pre-existing GitHub Copilot self-heal targets
(isGitHubAccessTokenOnlyConnection + canClearGitHubNoRefreshTokenState,
~line 83-97 / 413-492): a Copilot connection with no OAuth refresh
token but a still-valid copilotToken, which the sweep is supposed to
flip back to "active". With the new guard placed ahead of that block
unconditionally, the self-heal became unreachable for exactly the
state it exists to clear.

Impact: healthy GitHub Copilot connections that once lost their OAuth
refresh token got stuck at testStatus="expired" in the dashboard
forever, even though their Copilot sub-token kept working and the
sweep would have cleared the stale status back to "active" every
cycle before #8182.

Fix: carve out the exact recoverable shape from the terminal-skip
guard — testStatus==="expired" && errorCode==="no_refresh_token" &&
isGitHubAccessTokenOnlyConnection(conn) — so the guard still skips
every other terminal case (credits_exhausted, banned, and "expired"
for any other reason) untouched, matching #8182's original intent.

Validation: tests/unit/token-health-no-refresh-token-expired-5326.test.ts
was red (1 fail / 4 pass) before the fix — "checkConnection clears
stale no_refresh_token state for usable GitHub Copilot connections"
asserted testStatus flips back to "active" but got "expired". Green
after the fix (6/6, including a new boundary test proving a GitHub
Copilot connection expired for any OTHER reason, e.g. errorCode
"invalid_grant", is still skipped untouched). Also reran the adjacent
checkConnection/tokenHealthCheck suites (token-health-check.test.ts,
token-health-check-circuit-breaker.test.ts,
apikey-connection-health-check.test.ts, token-health-check-sweep.test.ts,
token-health-check-tickms-defined.test.ts, tokenHealthCheck-batchSize.test.ts,
codex-oauth-refresh-persist-6352.test.ts, oauth-providers-error-handling.test.ts)
— all green, confirming #8182's terminal-skip behavior is otherwise
unchanged. npm run typecheck:core clean.

Refs #8182
Refs #8286
Refs #5326
2026-07-24 09:56:58 -03:00
Diego Rodrigues de Sa e Souza
d095555d68 fix(sse): gate reasoning-placeholder strip to chunks that contain the sentinel (#8382)
Regression: #8162 (port of #8081) added an unconditional `.trim()` to
stripInternalReasoningPlaceholder(), applied to every streaming
delta.content chunk across 3 call-sites (openai-to-claude.ts,
openai-responses.ts, responsesTransformer.ts). Leading/trailing
whitespace at a chunk boundary is a real word boundary between
streaming fragments; trimming it glues adjacent chunks together on
the client ("Hello, " + "world." + " Bye." -> "Hello,world.Bye.").

Fix: early-return via .includes() before the replaceAll+trim, so the
function is a true no-op when the sentinel is absent from the chunk.
Behavior when the sentinel IS present is unchanged.

Validation:
- tests/unit/streaming-reasoning-dedup-5786.test.ts: the "(A-guard)"
  test was RED on the base branch ('Hello,world.Bye.' vs
  'Hello, world. Bye.'); GREEN after the fix (4/4 passing).
- tests/unit/translator-resp-openai-to-claude.test.ts: added a new
  multi-chunk boundary-whitespace regression test, proven RED against
  the pre-fix code (12/13), GREEN after (13/13).
- No regressions in responses-transformer.test.ts (17/17),
  responses-transformer-dense-output.test.ts (3/3), or the other
  suites exercising the shared placeholder utility (160/160 total
  across all consumers).

Refs #8162
Refs #8081
2026-07-24 09:56:54 -03:00
Diego Rodrigues de Sa e Souza
3b4f4afc9d fix(sse): re-export PROVIDER_BREAKER_FAILURE_STATUSES for the orphaned all-rate-limited breaker path (#8390)
Root cause: #8013 extracted shouldTripProviderBreakerForResult() from
src/sse/handlers/chat.ts into the new src/sse/handlers/chatPredicates.ts,
taking the (non-exported) const PROVIDER_BREAKER_FAILURE_STATUSES with it.
A second, independent use of that const survived in chat.ts's
handleSingleModelChat(), in the "all credentials rate-limited" block
(~line 1340) — that reference was left orphaned by the extraction.

Production impact: any request where every credential for a
provider+model is simultaneously rate-limited throws
`ReferenceError: PROVIDER_BREAKER_FAILURE_STATUSES is not defined` at
runtime in that code path. Concretely this meant:
- breaker._onFailure() was unreachable on the all-rate-limited path, so
  the provider circuit breaker could not trip from it
- the ReferenceError propagated up and got mapped to a generic 502,
  masking the real 503 upstream-unavailable status in combo responses
- the issue-agent route surfaced a generic 400 instead of the actual
  429 provider-rate-limited response

Fix: export PROVIDER_BREAKER_FAILURE_STATUSES from chatPredicates.ts and
add it to chat.ts's existing import block from that module. No behavior
change — the classification set ([408, 500, 502, 503, 504]) is
unchanged, this only repairs the broken reference.

Also re-points tests/unit/nvidia-quota-phase1.test.ts's regex-based
declaration check at chatPredicates.ts, where the const now actually
lives (it previously read chat.ts via fs+regex and silently failed to
find the declaration). The regex and the classification assertions
themselves are unchanged — this test still proves 429 is excluded from
the whole-provider breaker.

Refs #8013
2026-07-24 09:56:51 -03:00
Diego Rodrigues de Sa e Souza
3504050fcf fix(providers): route noauth opencode-zen connections through their assigned proxy (#8324) 2026-07-24 09:36:43 -03:00
Diego Rodrigues de Sa e Souza
7202654f81 fix(providers): classify per-model-quota 403 and DEGRADED 400 as model-unhealthy in checkFallbackError (#8247, #8248) (#8323) 2026-07-24 09:36:34 -03:00
Diego Rodrigues de Sa e Souza
ff3d3762af fix(api): fold namespace into the flattened Chat tool name so cross-namespace leaves do not collide (#8322) 2026-07-24 09:36:26 -03:00
Diego Rodrigues de Sa e Souza
35541c06cd fix(providers): carve cookie-auth providers out of terminal 401 'expired' classification so one 401 cooldowns instead of killing the connection (#8321) 2026-07-24 09:36:17 -03:00
Diego Rodrigues de Sa e Souza
dcbea8eb0d fix(providers): classify HTTP 400 model-unavailable as MODEL_NOT_FOUND so Antigravity Pro fallback locks out the deprecated model (#8319) 2026-07-24 09:36:08 -03:00
Ridho Pratama
ddbd054e49 fix(sse): preserve Responses combo payloads (#8310) 2026-07-24 09:36:00 -03:00
Prudhvi Vuda
14f4c67598 fix(sse): suppress </think> by default on Chat Completions (#8245) (#8309)
Claude→OpenAI translation was emitting a literal </think> into
delta.content for ordinary Chat Completions clients. Reasoning already
ships as reasoning_content, so default to suppress and keep
x-omniroute-thinking-marker: on as the #4633 opt-in.
2026-07-24 09:35:51 -03:00
Ravi Tharuma
1f7ec2c321 fix(cpa): isolate credential-pool failures (#8308)
* fix(cpa): isolate credential pool failures

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(cpa): forward transport through the chatCore key-health wrapper

The local recordKeyHealthStatus wrapper in handleChatCore only declared
(status, creds), so the transport argument added for CPA credential-pool
isolation was silently dropped at the call site (TS2554 "Expected 2
arguments, but got 3" once chatCore.ts is typechecked with tsc directly —
this file is not in tsconfig.typecheck-core.json's file list, so `npm run
typecheck:core` did not surface it). The CPA isolation guard in
keyHealth.ts never received `transport`, so it never fired.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-24 09:35:43 -03:00
Ravi Tharuma
cbe49f6929 fix(mcp): keep POST SSE responses uncompressed (#8303)
Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-07-24 09:35:35 -03:00
Ravi Tharuma
1684adcd63 fix(providers): adapt Kimi nonstream requests internally (#8302)
Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-07-24 09:35:26 -03:00
Ravi Tharuma
8a2a3d48b0 perf(api): singleflight version lookups (#8278) (#8301)
Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-07-24 09:35:18 -03:00
backryun
b84f86ad4f fix(runtime): isolate unique 8177 repairs (#8298) 2026-07-24 09:35:10 -03:00
diegosouzapw
0b68fd353f Add comparison and zero-config installation diagrams in SVG format
- Created a comparison table SVG illustrating the capabilities of OmniRoute versus competitors (9router, OpenRouter, CLIProxyAPI, LiteLLM) across 13 features.
- Added a zero-config installation SVG demonstrating the ease of setting up OmniRoute with three simple steps: installation, pointing to the tool, and receiving instant replies.
2026-07-24 09:24:38 -03:00
Diego Rodrigues de Sa e Souza
353ddc5cb1 docs: sync env-var contract (chaos panel, notion TLS, grok auth path) + repair glued VNC line in .env.example (#8362) 2026-07-24 07:44:31 -03:00
Diego Rodrigues de Sa e Souza
852bf4e0b0 docs: add public ROADMAP (3.8.5x rail -> 3.9.0 LTS -> 4.0 modular platform) (#8348) 2026-07-23 22:57:58 -03:00
diegosouzapw
2d789424f1 chore(quality): rebaseline file-size own-growth for merge-train 15 (auth/muse-spark/translator-test) 2026-07-23 20:14:04 -03:00
636 changed files with 30958 additions and 12325 deletions

View File

@@ -1027,7 +1027,7 @@ GITHUB_OAUTH_CLIENT_ID=Iv1.b507a08c87ecfe98
# Used by: open-sse/executors/base.ts — buildHeaders() dynamic lookup.
# Update these when providers release new CLI versions to avoid blocks.
CLAUDE_USER_AGENT="claude-cli/2.1.207 (external, cli)"
CLAUDE_USER_AGENT="claude-cli/2.1.219 (external, cli)"
# Disable the deterministic tool-name cloak applied on both Anthropic-bound paths
# (executors/base.ts native OAuth + executors/cliproxyapi.ts CLIProxyAPI) —
@@ -1176,6 +1176,20 @@ CURSOR_USER_AGENT="Cursor/3.4"
# OMNIROUTE_GROK_TLS_TIMEOUT_MS=60000
# OMNIROUTE_GROK_TLS_GRACE_MS=10000
# ── Notion web TLS sidecar (Chrome-fingerprinted client) ──
# Used by: open-sse/services/notionTlsClient.ts — wire-level timeout for the
# bogdanfinn/tls-client koffi binding and the JS-side grace window layered on
# top of it when the native library is wedged. The notion-web executor raises
# the wire timeout per-request to 180000 for long generations.
# OMNIROUTE_NOTION_TLS_TIMEOUT_MS=30000
# OMNIROUTE_NOTION_TLS_GRACE_MS=10000
# ── Grok web quota fetcher (auth.json override) ──
# Used by: open-sse/services/grokQuotaFetcher.ts — path of the Grok CLI
# auth.json used to fetch the grok-web weekly quota. Defaults to
# ~/.grok/auth.json; override for tests or a non-standard CLI install.
# GROK_AUTH_PATH=
# ── Browser-backed web-cookie chat (Playwright shared pool) ──
# Used by: open-sse/services/browserPool.ts + browserBackedChat.ts. The shared
# browser pool warms a headless context for web-cookie providers (e.g. claude-web)
@@ -2085,6 +2099,15 @@ QUOTA_STORE_DRIVER=sqlite # sqlite | redis
# the full (unfiltered) pool with a warning. Source: open-sse/services/autoCombo/virtualFactory.ts
# OMNIROUTE_AUTO_FREE_FALLBACK_TO_FULL_POOL=false
# ─── Auto-Combo chaos panel (broadcast variant) ────────────────────────────
# Tuning for the `auto/*:chaos` variant, which fans a single request out to a
# panel of provider-diverse models. Panel size is clamped to 1..10 (default 5);
# min-panel and the panel hard-timeout fall back to the engine defaults when
# unset. Source: open-sse/services/autoCombo/virtualFactory.ts
# OMNIROUTE_CHAOS_MAX_PANEL=5
# OMNIROUTE_CHAOS_MIN_PANEL=
# OMNIROUTE_CHAOS_PANEL_TIMEOUT_MS=
# ─── OpenCode config regeneration (scripts/ad-hoc/regen-opencode-config.ts) ───
# Base URL of the OmniRoute instance to query for /v1/models when regenerating
# an opencode.json with accurate limit.context values. Used by:
@@ -2252,7 +2275,8 @@ QUOTA_STORE_DRIVER=sqlite # sqlite | redis
# or lifecycle tuning.
# ─────────────────────────────────────────────────────────────────────────────
# OMNIROUTE_VNC_IMAGE=omniroute-vnc-chromium:local
# OMNIROUTE_DOCKER_BIN=docker# OMNIROUTE_VNC_CONTAINER_VNC_PORT=3000
# OMNIROUTE_DOCKER_BIN=docker
# OMNIROUTE_VNC_CONTAINER_VNC_PORT=3000
# OMNIROUTE_VNC_CONTAINER_CDP_PORT=9223
# OMNIROUTE_VNC_CONTAINER_PROFILE_DIR=/config
# OMNIROUTE_VNC_PROFILE_DIR=
@@ -2266,5 +2290,6 @@ QUOTA_STORE_DRIVER=sqlite # sqlite | redis
# ─────────────────────────────────────────────────────────────────────────────
# Data-dir alias (optional — open-sse/services/notionThreadSessions.ts)
# Legacy fallback for DATA_DIR, checked only after DATA_DIR and
# OMNIROUTE_DATA_DIR are both unset. Locates the Notion web-thread session cache.# ─────────────────────────────────────────────────────────────────────────────
# OMNIROUTE_DATA_DIR are both unset. Locates the Notion web-thread session cache.
# ─────────────────────────────────────────────────────────────────────────────
# VIBEPROXY_DATA_DIR=

View File

@@ -9,10 +9,12 @@
## Validation
Run only the focused loop for what you changed — the full unit suite, Vitest, the
60% coverage gate, and the production build all run in CI on this PR (#8329):
- [ ] Focused tests for the change: `node --import tsx/esm --test tests/unit/<file>.test.ts`
- [ ] `npm run lint`
- [ ] `npm run test:unit`
- [ ] `npm run test:coverage`
- [ ] Coverage is still `>= 60%` for statements, lines, functions, and branches
- [ ] Production-code changes include a new or updated automated test in this PR
- [ ] SonarQube PR analysis is green or any remaining issues are explicitly documented below
## Tests Added Or Updated

View File

@@ -477,6 +477,7 @@ jobs:
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: node scripts/i18n/check-glossary-consistency.mjs --locale=zh-CN
- run: node scripts/i18n/check-glossary-consistency.mjs --locale=zh-TW
# D4 (plano mestre testes+CI): a matrix de ~40 jobs de <1min por idioma saturava sozinha
# a concorrência de jobs da conta (Free = 20 slots, compartilhados entre TODOS os repos)
@@ -496,7 +497,7 @@ jobs:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-python@v6
- uses: actions/setup-python@v7
with:
python-version: "3.12"
- name: Validate all languages
@@ -907,7 +908,7 @@ jobs:
# (if-no-files-found: warn) — Sonar consumes the same file.
- name: Upload coverage to Codecov (informational)
if: always()
uses: codecov/codecov-action@0fb7174895f61a3b6b78fc075e0cd60383518dac # v5
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
files: coverage/lcov.info
token: ${{ secrets.CODECOV_TOKEN }}

View File

@@ -22,10 +22,10 @@ jobs:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: github/codeql-action/init@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
- uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
with:
languages: javascript-typescript
queries: security-extended
- uses: github/codeql-action/analyze@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
- uses: github/codeql-action/analyze@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
with:
category: "/language:javascript-typescript"

View File

@@ -2,8 +2,20 @@ name: DAST smoke (PR)
on:
pull_request:
branches: ["main", "release/**"]
# Runner-cost guard (#8084): the CLI-bundle build alone is 6-11min; a docs-only PR
# cannot change DAST behavior, so skip the whole workflow for pure docs/markdown
# changes. Any code path in the diff still runs the full smoke.
paths-ignore:
- "docs/**"
- "**/*.md"
permissions:
contents: read
# Superseded runs on the same PR must not stack 25-minute advisory builds
# (force-push storms were holding 2-3 runners each). Same group rule as quality.yml.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
dast-smoke:
runs-on: ubuntu-latest
@@ -41,7 +53,7 @@ jobs:
if curl -sf http://localhost:20128/api/monitoring/health >/dev/null; then echo up; break; fi
sleep 2
done
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
- run: pip install schemathesis

View File

@@ -90,7 +90,7 @@ jobs:
if curl -sf http://localhost:20128/api/monitoring/health >/dev/null; then echo up; break; fi
sleep 2
done
- uses: actions/setup-python@v6
- uses: actions/setup-python@v7
if: steps.gate.outputs.run == 'true'
with: { python-version: "3.12" }
- run: pip install garak

View File

@@ -35,7 +35,7 @@ jobs:
if curl -sf http://localhost:20128/api/monitoring/health >/dev/null; then echo "server up"; break; fi
sleep 2
done
- uses: actions/setup-python@v6
- uses: actions/setup-python@v7
with: { python-version: "3.12" }
- name: Install schemathesis
run: pip install schemathesis

View File

@@ -177,17 +177,26 @@ jobs:
git fetch --no-tags origin "$GITHUB_BASE_REF" || true
node scripts/quality/build-test-impact-map.mjs
SEL="$(node scripts/quality/select-impacted-tests.mjs)"
if [ -z "$SEL" ]; then echo "No source/test changes — skipping unit tests"; exit 0; fi
# Shadow evidence (#8084): persist every selection so TIA false negatives can
# be measured against fast-unit's full-suite verdict across releases BEFORE
# any gate authority moves off ordinary PRs. Artifact uploaded below.
printf '%s\n' "$SEL" > tia-selection.txt
if [ -z "$SEL" ]; then
echo "TIA selection: empty (no source/test changes)" >> "$GITHUB_STEP_SUMMARY"
echo "No source/test changes — skipping unit tests"; exit 0
fi
# CI runners are 4-vCPU; run at --test-concurrency=4 (matching the ci.yml unit
# job) rather than test:unit's local-tuned concurrency=20. Oversubscribing the
# runner makes timing-sensitive tests (db-backup, upstream-timeout, ...) flake,
# which must not happen on a blocking gate. DATA_DIR isolation keeps the parallel
# run race-free regardless of concurrency.
if echo "$SEL" | grep -q "__RUN_ALL__"; then
echo "TIA selection: __RUN_ALL__ (fail-safe) — full-suite authority stays with fast-unit" >> "$GITHUB_STEP_SUMMARY"
echo "Fail-safe: __RUN_ALL__ — deferring FULL unit suite to fast-unit (4-shard)."
echo "Not re-running unsharded test:unit:ci here (duplicate of fast-unit coverage)."
exit 0
fi
echo "TIA selection: $(grep -c . tia-selection.txt) impacted test file(s) — full suite still runs in fast-unit (shadow-evidence phase, #8084)" >> "$GITHUB_STEP_SUMMARY"
echo "Running impacted tests:"; echo "$SEL"
mapfile -t FILES <<< "$SEL"
# Loader parity with test:unit:ci:shard (#6787): tests/unit/dashboard/** runs
@@ -210,6 +219,16 @@ jobs:
node --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 "${DASH[@]}" || RC=$?
fi
exit $RC
# #8084 shadow evidence: keep the raw selection downloadable so TIA misses can be
# audited against fast-unit failures on the same run (gate moves need this data).
- name: Upload TIA selection (shadow evidence)
if: always()
uses: actions/upload-artifact@v7
with:
name: tia-selection
path: tia-selection.txt
if-no-files-found: ignore
retention-days: 30
fast-vitest:
name: Vitest (fast-path)

View File

@@ -26,7 +26,7 @@ jobs:
persist-credentials: false
- name: Run analysis
uses: ossf/scorecard-action@v2.4.3
uses: ossf/scorecard-action@v2.4.4
with:
results_file: results.sarif
results_format: sarif

View File

@@ -6,6 +6,12 @@ on:
branches: ["main"]
permissions:
contents: read
# Cancel superseded PR runs (same rule as quality.yml). No paths filter on purpose:
# p/secrets must keep scanning docs-only diffs too — credentials leak in .md files.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
semgrep:
runs-on: ubuntu-latest

View File

@@ -10,7 +10,8 @@ This plugin solves that by:
- Fetching `/v1/models` and `/api/combos` **at OpenCode startup, in Node.js** — no CORS, no WebView restrictions
- Emitting the provider block **dynamically** in the plugin's `config`/`provider` hook — so `opencode.json` only needs the plugin entry, not a static `provider.omniroute`
- Re-fetching on a configurable TTL (default 5 min), so new models / combo changes in the OmniRoute UI appear without restarting OpenCode
- Re-fetching on a configurable TTL (default 5 min) **and** background auto-discovery while OpenCode is running (`autoSyncIntervalMs`, default 5 min), so new models / combo changes appear without restarting OpenCode
- Exposing a force-refresh path (`omniroute_sync_models` tool + `/omni-sync` command template) equivalent to Pi `/omni sync`
- Computing `limit.context` for combos as `min(member.context_length)` from the live catalog (no more `null` values that cause 4K-token truncation)
- **Auto-pickup of `interleaved` capability** for thinking models (merged via PR #3138)
@@ -73,6 +74,9 @@ Peer dep: `@opencode-ai/plugin` (managed by your OpenCode install).
{
"providerId": "omniroute",
"baseURL": "https://or.example.com",
// Background re-discovery while OpenCode is running (Pi parity).
// Default 300000 (5 min). Minimum 60000. Set 0 to disable.
"autoSyncIntervalMs": 300000,
},
],
],
@@ -88,6 +92,27 @@ opencode auth login --provider omniroute
Restart OpenCode. `/models` lists the full live catalog. Variants (`-low`, `-medium`, `-high`, `-thinking`) and combos appear as first-class IDs — OmniRoute is the source of truth, no client-side synthesis.
### Live catalog refresh (auto + force)
While OpenCode is running, the plugin keeps the model catalog fresh in two ways:
| Mechanism | Default | What it does |
| --- | --- | --- |
| `modelCacheTtl` | `300000` (5 min) | On-demand TTL: next provider/models hook after expiry re-fetches `/v1/models` |
| `autoSyncIntervalMs` | `300000` (5 min) | Background timer: proactively invalidates + re-fetches while the harness is running. Min `60000`. Set `0` to disable background polling (TTL still applies) |
**Force sync now** (Pi `/omni sync` equivalent) — OpenCode has no Pi-style slash-command registration API, so the plugin wires both a tool and command templates:
1. **Tool:** `omniroute_sync_models` — invalidates in-memory + disk caches, re-fetches `GET /v1/models` (and combos/enrichment when enabled), returns `{ ok, count, ... }`.
2. **Command templates** (type these in OpenCode):
- `/omni-sync` — asks the agent to call `omniroute_sync_models` and report the result
- `/omni-autosync` — asks the agent to report current `autoSyncIntervalMs` / `modelCacheTtl` status
```text
/omni-sync
/omni-autosync
```
## Multi-instance (prod + preprod side-by-side)
> ⚠ OC ≤1.15.5 dedupes plugin loads by absolute module path. Two `plugin:` entries pointing at the same `dist/index.js` collapse into one (last-listed options win). Workaround: install the plugin twice into separate directories so each entry resolves to a distinct module file. v0.2.x will introduce an `instances: [...]` shape that registers N providers from a single load.

View File

@@ -1,6 +1,6 @@
{
"name": "@omniroute/opencode-plugin",
"version": "0.2.0",
"version": "0.2.1",
"description": "OpenCode plugin for the OmniRoute AI Gateway. Drives dynamic model discovery, /connect auth flow, and multi-instance OmniRoute providers via the official @opencode-ai/plugin contract.",
"type": "module",
"main": "./dist/index.js",
@@ -23,7 +23,7 @@
"scripts": {
"build": "tsup",
"clean": "rm -rf dist",
"test": "node --import tsx/esm --test tests/scaffold.test.ts tests/auth.test.ts tests/options-schema.test.ts tests/multi-instance.test.ts tests/fetch-interceptor.test.ts tests/provider.test.ts tests/gemini-sanitize.test.ts tests/combos.test.ts tests/config-shim.test.ts tests/features.test.ts tests/usable-combo.test.ts tests/disk-snapshot-perms.test.ts tests/fork-features.test.ts tests/auto-combo-context.test.ts tests/provider-id-routing.test.ts tests/management-read-token.test.ts",
"test": "node --import tsx/esm --test tests/scaffold.test.ts tests/auth.test.ts tests/options-schema.test.ts tests/multi-instance.test.ts tests/fetch-interceptor.test.ts tests/provider.test.ts tests/gemini-sanitize.test.ts tests/combos.test.ts tests/config-shim.test.ts tests/features.test.ts tests/usable-combo.test.ts tests/disk-snapshot-perms.test.ts tests/fork-features.test.ts tests/auto-combo-context.test.ts tests/provider-id-routing.test.ts tests/management-read-token.test.ts tests/auto-sync.test.ts",
"prepublishOnly": "npm run clean && npm run build && npm test"
},
"keywords": [

View File

@@ -46,7 +46,7 @@
*/
import { createHash } from "node:crypto";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
@@ -54,6 +54,7 @@ import { homedir } from "node:os";
import { join } from "node:path";
import { randomUUID } from "node:crypto";
import type { AuthHook, Config, Plugin, PluginOptions, ProviderHook } from "@opencode-ai/plugin";
import { tool } from "@opencode-ai/plugin";
import type { Model as ModelV2 } from "@opencode-ai/sdk/v2";
import { z } from "zod";
import { logger as _logger, setLogLevel, type LogLevel as _LogLevel } from "./logger.js";
@@ -88,6 +89,10 @@ import {
* from providerId.
* - `modelCacheTtl` `/v1/models` TTL cache duration in milliseconds.
* Default: 300_000 (5 min).
* - `autoSyncIntervalMs` Background catalog re-discovery while OpenCode is
* running. Default: 300_000 (5 min). Minimum: 60_000.
* Set `0` to disable background auto-sync (TTL on-demand
* discovery still applies via `modelCacheTtl`).
* - `baseURL` Override base URL for this OmniRoute instance. When
* absent, the loader falls back to a credential-attached
* baseURL set by `/connect`.
@@ -190,6 +195,12 @@ const optionsSchema = z
.optional(),
displayName: z.string().min(1).optional(),
modelCacheTtl: z.number().positive().optional(),
/**
* Background auto-discovery interval while the harness is running.
* `0` disables background polling. Values in (0, 60000) are clamped up
* to 60000. Default when unset: 300000.
*/
autoSyncIntervalMs: z.number().int().nonnegative().optional(),
baseURL: z.string().url().optional(),
managementReadToken: z.string().min(1).optional(),
features: featuresSchema.optional(),
@@ -217,6 +228,29 @@ export const PLUGIN_GIT_HASH: string =
export const DEFAULT_MODEL_CACHE_TTL_MS = 300_000 as const;
/** Default background auto-discovery interval (matches modelCacheTtl default). */
export const DEFAULT_AUTO_SYNC_INTERVAL_MS = 300_000 as const;
/** Minimum positive background auto-discovery interval. */
export const MIN_AUTO_SYNC_INTERVAL_MS = 60_000 as const;
/**
* Sanitize background auto-sync interval.
* - unset/invalid → default 300_000
* - explicit 0 → disabled
* - (0, 60000) → clamped to 60000
* - ≥ 60000 → kept as integer ms
*/
export function sanitizeAutoSyncIntervalMs(value: unknown): number {
if (value === undefined || value === null) return DEFAULT_AUTO_SYNC_INTERVAL_MS;
if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_AUTO_SYNC_INTERVAL_MS;
const n = Math.trunc(value);
if (n === 0) return 0;
if (n < 0) return DEFAULT_AUTO_SYNC_INTERVAL_MS;
if (n < MIN_AUTO_SYNC_INTERVAL_MS) return MIN_AUTO_SYNC_INTERVAL_MS;
return n;
}
// Manual trim helpers avoid polynomial-regex CodeQL warnings on
// user-supplied baseURL strings (string.replace(/\/+$/, "")). The same
// behaviour, no backtracking.
@@ -243,7 +277,7 @@ function trimLeadingDashes(value: string): string {
* sees a consistent identifier.
*/
export function resolveOmniRoutePluginOptions(opts?: OmniRoutePluginOptions): Required<
Pick<OmniRoutePluginOptions, "providerId" | "displayName" | "modelCacheTtl">
Pick<OmniRoutePluginOptions, "providerId" | "displayName" | "modelCacheTtl" | "autoSyncIntervalMs">
> & {
/**
* #6859: the UNPREFIXED provider id ("omniroute", "omniroute-preprod", …).
@@ -277,17 +311,22 @@ export function resolveOmniRoutePluginOptions(opts?: OmniRoutePluginOptions): Re
typeof opts?.modelCacheTtl === "number" && opts.modelCacheTtl > 0
? opts.modelCacheTtl
: DEFAULT_MODEL_CACHE_TTL_MS;
const autoSyncIntervalMs = sanitizeAutoSyncIntervalMs(opts?.autoSyncIntervalMs);
return {
providerId,
omnirouteProviderId,
displayName,
modelCacheTtl,
autoSyncIntervalMs,
baseURL: opts?.baseURL,
managementReadToken: opts?.managementReadToken,
features: opts?.features,
};
}
/** Fully resolved plugin options (defaults applied). */
export type ResolvedOmniRoutePluginOptions = ReturnType<typeof resolveOmniRoutePluginOptions>;
/**
* Strip a leading "opencode-" prefix (added only for the OC native-adapter
* gate — see `resolveOmniRoutePluginOptions`) so the returned id is safe to
@@ -523,8 +562,378 @@ export function createOmniRouteAuthHook(opts?: OmniRoutePluginOptions): AuthHook
* opencode.json), NOT as a closure binding. Multi-instance support follows
* from each plugin tuple invoking the factory with its own opts.
*/
/**
* Invalidate in-memory fetch cache entries for a baseURL (all credential keys).
* Returns number of entries removed.
*/
export function invalidateOmniRouteFetchCache(
cache: OmniRouteFetchCache,
baseURL?: string,
): number {
if (!baseURL) {
const n = cache.size;
cache.clear();
return n;
}
const prefix = `${baseURL}::`;
let removed = 0;
for (const key of [...cache.keys()]) {
if (key.startsWith(prefix) || key === baseURL) {
cache.delete(key);
removed += 1;
}
}
return removed;
}
/**
* Resolve API credentials for force-sync / background refresh without
* depending on the provider.models auth context.
*/
export async function resolveOmniRouteRuntimeAuth(
resolved: ResolvedOmniRoutePluginOptions,
readAuthJson?: OmniRouteReadAuthJson,
): Promise<{ apiKey: string; baseURL: string; managementReadToken: string } | null> {
const reader = readAuthJson ?? defaultReadAuthJson;
let authJson: AuthJsonShape | undefined | null;
try {
authJson = await reader();
} catch {
authJson = undefined;
}
if (authJson === null) authJson = undefined;
const bareKey = resolved.providerId.startsWith("opencode-")
? resolved.providerId.slice("opencode-".length)
: resolved.providerId;
const lookupKeys = [resolved.providerId];
if (bareKey !== resolved.providerId) lookupKeys.push(bareKey);
if (resolved.omnirouteProviderId && !lookupKeys.includes(resolved.omnirouteProviderId)) {
lookupKeys.push(resolved.omnirouteProviderId);
}
let entry: AuthJsonApiEntry | undefined;
for (const k of lookupKeys) {
const e = authJson?.[k];
if (
e &&
(e as { type?: unknown }).type === "api" &&
typeof (e as { key?: unknown }).key === "string" &&
((e as { key: string }).key).length > 0
) {
entry = e as AuthJsonApiEntry;
break;
}
}
const apiKey = entry?.key ?? "";
if (!apiKey) return null;
const authBaseURL =
entry && typeof (entry as { baseURL?: unknown }).baseURL === "string"
? (entry as { baseURL: string }).baseURL
: "";
const baseURL = resolved.baseURL ?? (authBaseURL || "");
if (!baseURL) return null;
const managementReadToken = resolved.managementReadToken ?? apiKey;
return { apiKey, baseURL, managementReadToken };
}
/**
* Force-refresh OmniRoute catalog: clear memory + disk cache, re-fetch /v1/models
* (and optional management endpoints), and repopulate the shared cache.
* OpenCode equivalent of Pi `/omni sync`.
*/
export async function forceSyncOmniRouteModels(args: {
resolved: ResolvedOmniRoutePluginOptions;
cache: OmniRouteFetchCache;
readAuthJson?: OmniRouteReadAuthJson;
fetcher?: OmniRouteModelsFetcher;
combosFetcher?: OmniRouteCombosFetcher;
autoCombosFetcher?: OmniRouteAutoCombosFetcher;
enrichmentFetcher?: OmniRouteEnrichmentFetcher;
compressionMetaFetcher?: OmniRouteCompressionMetaFetcher;
providersFetcher?: OmniRouteProvidersFetcher;
now?: () => number;
}): Promise<{
ok: boolean;
count: number;
combos: number;
provider: string;
baseURL?: string;
clearedMemory: number;
clearedDisk: boolean;
error?: string;
}> {
const resolved = args.resolved;
const cache = args.cache;
const now = args.now ?? Date.now;
const fetcher = args.fetcher ?? defaultOmniRouteModelsFetcher;
const combosFetcher = args.combosFetcher ?? defaultOmniRouteCombosFetcher;
const autoCombosFetcher = args.autoCombosFetcher ?? defaultOmniRouteAutoCombosFetcher;
const enrichmentFetcher = args.enrichmentFetcher ?? defaultOmniRouteEnrichmentFetcher;
const compressionMetaFetcher =
args.compressionMetaFetcher ?? defaultOmniRouteCompressionMetaFetcher;
const providersFetcher = args.providersFetcher ?? defaultOmniRouteProvidersFetcher;
const features = resolved.features ?? {};
const wantCombos = features.combos !== false;
const wantAutoCombos = features.autoCombos !== false;
const wantEnrichment = features.enrichment !== false;
const wantCompressionMeta = features.compressionMetadata === true;
const wantUsableOnly = features.usableOnly === true;
const wantDiskCache = features.diskCache !== false;
const auth = await resolveOmniRouteRuntimeAuth(
resolved,
args.readAuthJson ?? defaultReadAuthJson,
);
if (!auth) {
return {
ok: false,
count: 0,
combos: 0,
provider: resolved.omnirouteProviderId,
clearedMemory: 0,
clearedDisk: false,
error:
"No OmniRoute credentials/baseURL available. Run `opencode connect omniroute` or set plugin baseURL.",
};
}
const clearedMemory = invalidateOmniRouteFetchCache(cache, auth.baseURL);
// Clear residual entries from prior baseURL history as well.
const clearedAll = invalidateOmniRouteFetchCache(cache);
let clearedDisk = false;
if (wantDiskCache) {
clearedDisk = await clearDiskSnapshot(resolved.providerId);
if (resolved.omnirouteProviderId !== resolved.providerId) {
clearedDisk = (await clearDiskSnapshot(resolved.omnirouteProviderId)) || clearedDisk;
}
}
try {
const rawModels = await fetcher(auth.baseURL, auth.apiKey, 10_000);
let rawCombos: OmniRouteRawCombo[] = [];
if (wantCombos) {
try {
rawCombos = await combosFetcher(auth.baseURL, auth.managementReadToken, 10_000);
} catch (err) {
console.warn("[omniroute-plugin] force sync: combos fetch failed", err);
}
}
let rawAutoCombos: OmniRouteRawAutoCombo[] = [];
if (wantAutoCombos) {
try {
rawAutoCombos = await autoCombosFetcher(auth.baseURL, auth.managementReadToken, 5_000);
} catch {
/* soft-fail */
}
}
let rawEnrichment: OmniRouteEnrichmentMap = new Map();
if (wantEnrichment) {
try {
rawEnrichment = await enrichmentFetcher(auth.baseURL, auth.managementReadToken, 10_000);
} catch {
rawEnrichment = new Map();
}
}
let rawCompressionCombos: OmniRouteCompressionCombo[] = [];
if (wantCompressionMeta) {
try {
rawCompressionCombos = await compressionMetaFetcher(
auth.baseURL,
auth.managementReadToken,
10_000,
);
} catch {
rawCompressionCombos = [];
}
}
let rawConnections: OmniRouteProviderConnection[] = [];
if (wantUsableOnly) {
try {
rawConnections = await providersFetcher(auth.baseURL, auth.managementReadToken, 10_000);
} catch {
rawConnections = [];
}
}
const t = now();
const entry = {
rawModels,
rawCombos,
rawAutoCombos,
rawEnrichment,
rawCompressionCombos,
rawConnections,
expiresAt: t + resolved.modelCacheTtl,
};
const cacheKey = modelsCacheKey(
auth.baseURL,
`${auth.apiKey}\0${auth.managementReadToken}`,
);
cache.set(cacheKey, entry);
if (wantDiskCache) {
try {
const fingerprint = diskSnapshotIdentityFingerprint(
auth.baseURL,
auth.apiKey,
auth.managementReadToken,
);
const { expiresAt: _expiresAt, ...diskEntry } = entry;
await defaultDiskSnapshotWriter(resolved.providerId, diskEntry, fingerprint);
} catch {
/* soft-fail disk write */
}
}
console.warn(
`[omniroute-plugin] force sync ok providerId=${resolved.providerId} ` +
`models=${rawModels.length} combos=${rawCombos.length} ` +
`clearedMemory=${clearedMemory + clearedAll} disk=${clearedDisk}`,
);
return {
ok: true,
count: rawModels.length,
combos: rawCombos.length,
provider: resolved.omnirouteProviderId,
baseURL: auth.baseURL,
clearedMemory: clearedMemory + clearedAll,
clearedDisk,
};
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return {
ok: false,
count: 0,
combos: 0,
provider: resolved.omnirouteProviderId,
baseURL: auth.baseURL,
clearedMemory: clearedMemory + clearedAll,
clearedDisk,
error: message,
};
}
}
export function createOmniRouteSyncModelsTool(args: {
resolved: ResolvedOmniRoutePluginOptions;
cache: OmniRouteFetchCache;
}): ReturnType<typeof tool> {
const { resolved, cache } = args;
return tool({
description:
"Force-refresh the OmniRoute model catalog (OpenCode equivalent of Pi `/omni sync`). " +
"Invalidates in-memory and disk caches, then re-fetches GET /v1/models (and combos when enabled).",
args: {
reason: tool.schema
.string()
.optional()
.describe("Optional reason for the sync (logging only)"),
},
async execute(toolArgs) {
const result = await forceSyncOmniRouteModels({ resolved, cache });
const reason = toolArgs.reason ? ` reason=${toolArgs.reason}` : "";
if (!result.ok) {
return {
title: "OmniRoute sync failed",
output:
`OmniRoute model sync failed for provider=${result.provider}.${reason}\n` +
`${result.error ?? "unknown error"}`,
metadata: result,
};
}
return {
title: `OmniRoute sync: ${result.count} models`,
output:
`OmniRoute models synced.` +
`\nprovider: ${result.provider}` +
`\nbaseURL: ${result.baseURL}` +
`\nmodels: ${result.count}` +
`\ncombos: ${result.combos}` +
`\nclearedMemoryEntries: ${result.clearedMemory}` +
`\nclearedDiskSnapshot: ${result.clearedDisk}` +
`\nTTL: ${resolved.modelCacheTtl}ms` +
`\nautoSyncIntervalMs: ${resolved.autoSyncIntervalMs}` +
reason,
metadata: result,
};
},
});
}
/**
* Start background auto-discovery while the harness is running.
* Quiet: only logs when the model count changes or on errors.
* Returns a stop function.
*/
export function startOmniRouteAutoSync(args: {
resolved: ResolvedOmniRoutePluginOptions;
cache: OmniRouteFetchCache;
intervalMs?: number;
}): () => void {
const resolved = args.resolved;
const cache = args.cache;
const intervalMs = args.intervalMs ?? resolved.autoSyncIntervalMs;
if (!intervalMs || intervalMs <= 0) {
return () => {};
}
let stopped = false;
let inFlight: Promise<void> | null = null;
let lastCount: number | undefined;
const tick = () => {
if (stopped) return;
if (inFlight) return;
inFlight = (async () => {
const result = await forceSyncOmniRouteModels({ resolved, cache });
if (!result.ok) {
console.warn(
`[omniroute-plugin] auto-sync failed providerId=${resolved.providerId}: ${result.error}`,
);
return;
}
if (lastCount === undefined) {
lastCount = result.count;
return;
}
if (result.count !== lastCount) {
console.warn(
`[omniroute-plugin] auto-sync catalog size changed ${lastCount}${result.count} ` +
`(providerId=${resolved.providerId})`,
);
lastCount = result.count;
}
})()
.catch((err) => {
console.warn("[omniroute-plugin] auto-sync tick error", err);
})
.finally(() => {
inFlight = null;
});
};
// Delay first background tick by one interval so session start is not doubled
// with the normal provider.models cold fetch. Manual tool remains immediate.
const timer = setInterval(tick, intervalMs);
if (typeof timer === "object" && timer && "unref" in timer && typeof timer.unref === "function") {
timer.unref();
}
console.warn(
`[omniroute-plugin] auto-sync enabled intervalMs=${intervalMs} providerId=${resolved.providerId}`,
);
return () => {
stopped = true;
clearInterval(timer);
};
}
export const OmniRoutePlugin: Plugin = async (_input, options) => {
const resolved = coercePluginOptions(options);
const resolved = resolveOmniRoutePluginOptions(coercePluginOptions(options));
// T-07: a single per-plugin-instance cache shared between the provider
// hook (T-03/T-05) and the config-shim hook (T-07). On OC ≥1.14.49 both
// hooks fire within the same Plugin invocation, so a shared cache keeps
@@ -553,10 +962,53 @@ export const OmniRoutePlugin: Plugin = async (_input, options) => {
// Wire log level: startupDebug:true → "debug", explicit logLevel wins.
setLogLevel(resolved.features?.startupDebug ? "debug" : (resolved.features?.logLevel ?? "warn"));
// Background auto-discovery while the harness is running (Pi parity).
// Interval 0 disables. TTL on-demand discovery still works via modelCacheTtl.
startOmniRouteAutoSync({ resolved, cache: sharedCache });
const syncTool = createOmniRouteSyncModelsTool({ resolved, cache: sharedCache });
const bareProviderId = resolved.omnirouteProviderId;
// Config hook: keep existing catalog shim, and register slash command
// templates that ask the agent to call the force-sync tool (OpenCode has no
// Pi-style registerCommand API; tools + command templates are the native path).
const baseConfigHook = createOmniRouteConfigHook(resolved, { cache: sharedCache });
const configWithSyncCommand = async (input: Config) => {
await baseConfigHook(input);
const cfg = input as Config & {
command?: Record<
string,
{ template: string; description?: string; agent?: string; model?: string; subtask?: boolean }
>;
};
if (!cfg.command) cfg.command = {};
if (!cfg.command["omni-sync"]) {
cfg.command["omni-sync"] = {
description: "Force-refresh OmniRoute model catalog (like Pi /omni sync)",
template:
`Force-refresh the OmniRoute model catalog now using the omniroute_sync_models tool ` +
`(provider ${bareProviderId}). After the tool returns, briefly report model count and whether the sync succeeded.`,
};
}
if (!cfg.command["omni-autosync"]) {
cfg.command["omni-autosync"] = {
description: "Show OmniRoute auto-sync / cache status",
template:
`Report OmniRoute discovery status for provider ${bareProviderId}: ` +
`autoSyncIntervalMs=${resolved.autoSyncIntervalMs}, modelCacheTtl=${resolved.modelCacheTtl}. ` +
`If the user asked to refresh now, call omniroute_sync_models.`,
};
}
};
return {
auth: createOmniRouteAuthHook(resolved),
provider: createOmniRouteProviderHook(resolved, { cache: sharedCache }),
config: createOmniRouteConfigHook(resolved, { cache: sharedCache }),
config: configWithSyncCommand,
tool: {
omniroute_sync_models: syncTool,
},
};
};
@@ -4061,6 +4513,19 @@ export function diskSnapshotPath(providerId: string): string {
return path.join(dir, "plugins", `omniroute-${providerId}.json`);
}
/** Best-effort delete of the disk snapshot for a provider (force-sync). */
export async function clearDiskSnapshot(providerId: string): Promise<boolean> {
const file = diskSnapshotPath(providerId);
try {
await unlink(file);
return true;
} catch (err) {
const code = (err as NodeJS.ErrnoException | undefined)?.code;
if (code === "ENOENT") return false;
return false;
}
}
export type OmniRouteDiskSnapshotWriter = (
providerId: string,
entry: Omit<OmniRouteFetchCacheEntry, "expiresAt">,

View File

@@ -0,0 +1,130 @@
/**
* Auto-discovery + force-sync (OpenCode parity with Pi `/omni sync`).
*/
import test from "node:test";
import assert from "node:assert/strict";
import {
sanitizeAutoSyncIntervalMs,
DEFAULT_AUTO_SYNC_INTERVAL_MS,
MIN_AUTO_SYNC_INTERVAL_MS,
parseOmniRoutePluginOptions,
resolveOmniRoutePluginOptions,
invalidateOmniRouteFetchCache,
forceSyncOmniRouteModels,
type OmniRouteFetchCache,
} from "../src/index.js";
test("sanitizeAutoSyncIntervalMs: unset → default 300000", () => {
assert.equal(sanitizeAutoSyncIntervalMs(undefined), DEFAULT_AUTO_SYNC_INTERVAL_MS);
assert.equal(sanitizeAutoSyncIntervalMs(null), DEFAULT_AUTO_SYNC_INTERVAL_MS);
});
test("sanitizeAutoSyncIntervalMs: 0 disables", () => {
assert.equal(sanitizeAutoSyncIntervalMs(0), 0);
});
test("sanitizeAutoSyncIntervalMs: clamps below min to 60000", () => {
assert.equal(sanitizeAutoSyncIntervalMs(1), MIN_AUTO_SYNC_INTERVAL_MS);
assert.equal(sanitizeAutoSyncIntervalMs(59_999), MIN_AUTO_SYNC_INTERVAL_MS);
});
test("sanitizeAutoSyncIntervalMs: keeps valid values", () => {
assert.equal(sanitizeAutoSyncIntervalMs(60_000), 60_000);
assert.equal(sanitizeAutoSyncIntervalMs(300_000), 300_000);
});
test("parseOmniRoutePluginOptions accepts autoSyncIntervalMs including 0", () => {
assert.equal(parseOmniRoutePluginOptions({ autoSyncIntervalMs: 0 }).autoSyncIntervalMs, 0);
assert.equal(parseOmniRoutePluginOptions({ autoSyncIntervalMs: 120_000 }).autoSyncIntervalMs, 120_000);
});
test("resolveOmniRoutePluginOptions defaults autoSyncIntervalMs to 300000", () => {
const r = resolveOmniRoutePluginOptions({});
assert.equal(r.autoSyncIntervalMs, DEFAULT_AUTO_SYNC_INTERVAL_MS);
});
test("resolveOmniRoutePluginOptions clamps low positive autoSyncIntervalMs", () => {
const r = resolveOmniRoutePluginOptions({ autoSyncIntervalMs: 5000 });
assert.equal(r.autoSyncIntervalMs, MIN_AUTO_SYNC_INTERVAL_MS);
});
test("invalidateOmniRouteFetchCache clears by baseURL prefix", () => {
const cache: OmniRouteFetchCache = new Map();
cache.set("https://a.example/v1::abc", {
rawModels: [],
rawCombos: [],
rawAutoCombos: [],
rawEnrichment: new Map(),
rawCompressionCombos: [],
rawConnections: [],
expiresAt: Date.now() + 1000,
});
cache.set("https://b.example/v1::def", {
rawModels: [],
rawCombos: [],
rawAutoCombos: [],
rawEnrichment: new Map(),
rawCompressionCombos: [],
rawConnections: [],
expiresAt: Date.now() + 1000,
});
const removed = invalidateOmniRouteFetchCache(cache, "https://a.example/v1");
assert.equal(removed, 1);
assert.equal(cache.size, 1);
assert.equal(cache.has("https://b.example/v1::def"), true);
});
test("forceSyncOmniRouteModels: fetches, populates cache, returns count", async () => {
const cache: OmniRouteFetchCache = new Map();
const resolved = resolveOmniRoutePluginOptions({
providerId: "omniroute",
baseURL: "https://omniroute.example/v1",
autoSyncIntervalMs: 0,
features: {
combos: false,
autoCombos: false,
enrichment: false,
compressionMetadata: false,
usableOnly: false,
diskCache: false,
},
});
const result = await forceSyncOmniRouteModels({
resolved,
cache,
readAuthJson: async () => ({
omniroute: { type: "api", key: "test-key" },
}),
fetcher: async () => [
{ id: "model-a", object: "model" },
{ id: "model-b", object: "model" },
],
now: () => 1_000_000,
});
assert.equal(result.ok, true);
assert.equal(result.count, 2);
assert.equal(result.provider, "omniroute");
assert.equal(cache.size, 1);
const entry = [...cache.values()][0];
assert.equal(entry.rawModels.length, 2);
assert.equal(entry.expiresAt, 1_000_000 + resolved.modelCacheTtl);
});
test("forceSyncOmniRouteModels: missing auth returns error", async () => {
const cache: OmniRouteFetchCache = new Map();
const resolved = resolveOmniRoutePluginOptions({
providerId: "omniroute",
baseURL: "https://omniroute.example/v1",
autoSyncIntervalMs: 0,
features: { diskCache: false },
});
const result = await forceSyncOmniRouteModels({
resolved,
cache,
readAuthJson: async () => ({}),
});
assert.equal(result.ok, false);
assert.match(result.error ?? "", /credentials|baseURL|connect/i);
});

View File

@@ -570,12 +570,16 @@ This repository is a fork of `diegosouzapw/OmniRoute`. Keep fork-only operationa
changes (for example GHCR image publishing, personal deployment workflows, or local
automation) out of upstream contribution PRs.
When preparing a PR for upstream, always start the work branch from `upstream/main`,
not from this fork's `main`:
When preparing a PR for upstream, always start the work branch from the upstream
**default branch** — the active `release/vX.Y.Z` line (today `release/v3.8.49`).
Never branch from `main`: `main` only receives release squash-merges, so a branch
cut there is weeks behind and produces conflict-heavy PRs
(see `CONTRIBUTING.md` and `docs/ops/BRANCHING_MODEL.md`):
```bash
git fetch upstream
git switch -c <branch-name> upstream/main
# the default branch is the active release line, e.g. release/v3.8.49
git switch -c <branch-name> upstream/release/vX.Y.Z
```
Only cherry-pick or reapply the changes intended for the upstream PR.

View File

@@ -198,11 +198,14 @@ Coverage notes:
### Pull Request Requirements
Before opening or merging a PR:
Before opening a PR, run the focused loop for what you changed. The full unit suite
(4 CI shards), Vitest, the **60%+** coverage gate, and the production build are CI's
responsibility — running them locally adds no signal the PR checks will not already
give you, and on smaller machines it can saturate the host (#8084):
- Run `npm run test:unit`
- Run `npm run test:coverage`
- Ensure the coverage gate stays at **60%+** statements/lines/functions/branches
- Run the test files that cover your change: `node --import tsx/esm --test tests/unit/<file>.test.ts`
- Run `npm run lint`
- Include or update automated tests in the same PR whenever production code changes
- Include the changed or added test files in the PR description when production code changed
- Check the SonarQube result on the PR when the project secrets are configured in CI
@@ -228,6 +231,31 @@ Current test status: **122 unit test files** covering:
- **Zod validation** — Use Zod v4 schemas for all API input validation
- **Naming**: Files = camelCase/kebab-case, components = PascalCase, constants = UPPER_SNAKE
### Error handling / empty catch blocks
Never leave a `catch` unexplained. Classify it into one of two buckets (operationalizes
the hard rule "never silently swallow errors in SSE streams"):
- **Intentional (our own best-effort cleanup/telemetry)** — a failure here is expected and
harmless; add a one-line rationale comment, no logging (logging on every request is the
noise this convention avoids).
```ts
} catch {} // closing an already-closed controller after client disconnect is expected
```
- **Should log (external/caller-supplied code, or the swallow changes control flow)** — keep
the catch (never let it break the stream) but emit a contextual `console.debug`/`warn` so the
failure is discoverable.
```ts
} catch (e) {
console.debug("[STREAM] onFailure callback error:", e);
}
```
See `open-sse/utils/stream.ts` and `open-sse/utils/streamHandler.ts` for applied examples.
---
## Project Structure

View File

@@ -27,7 +27,7 @@ When creating _any_ validation tests or one-off logic scripts, default to using
7. **Never bypass Husky hooks** (`--no-verify`, `--no-gpg-sign`) without explicit operator approval.
8. **Always validate inputs with Zod schemas** from `src/shared/validation/schemas.ts`.
9. **Always include tests when changing production code** (`src/`, `open-sse/`, `electron/`, `bin/`).
10. **Coverage must stay**75 % statements / 75 % lines / 75 % functions / 70 % branches (real measured: ~82 %).
10. **Coverage must stay**60 % statements / lines / functions / branches — the official CI gate (`npm run test:coverage`). The ratchet baseline in `quality-baseline.json` may freeze a higher floor; never regress it.
## 3. Codebase navigation

836
README.md

File diff suppressed because it is too large Load Diff

View File

@@ -78,18 +78,15 @@ export function registerBackup(program) {
if (exitCode !== 0) process.exit(exitCode);
});
// Legacy: `omniroute backup` without subcommand still creates a backup
// Legacy: `omniroute backup` without a subcommand still creates a backup
// (documented as the canonical usage in USER_GUIDE.md / CLI-TOOLS.md /
// AGENT-SKILLS.md). No flags are declared here — declaring the same
// option names as `create`/`auto enable` here previously shadowed them
// (#8512), and no doc shows `omniroute backup` invoked with flags.
backup.action(async (opts) => {
const exitCode = await runBackupCommand(opts);
if (exitCode !== 0) process.exit(exitCode);
});
backup
.option("--name <name>", t("backup.nameOpt"))
.option("--cloud", t("backup.cloudOpt"))
.option("--encrypt", t("backup.encryptOpt"))
.option("--key-file <path>", t("backup.keyFileOpt"))
.option("--exclude <pattern>", t("backup.excludeOpt"), (v, prev = []) => [...prev, v], [])
.option("--retention <n>", t("backup.retentionOpt"), parseInt);
}
export function registerRestore(program) {

View File

@@ -26,6 +26,7 @@ function wantsProviderSetup(opts) {
async function resolvePassword(opts, prompt, nonInteractive) {
if (opts.password) return opts.password;
if (process.env.INITIAL_PASSWORD) return process.env.INITIAL_PASSWORD;
if (nonInteractive) return "";
const answer = await prompt.ask("Set an admin password now? [y/N]", "N");

View File

@@ -5,15 +5,34 @@ import { ensureSettingsSchema, hashManagementPassword, updateSettings } from "./
async function loadSqlite() {
if (process.versions.bun) {
return (await import("bun:sqlite")).Database;
return { Database: (await import("bun:sqlite")).Database };
}
try {
return (await import("better-sqlite3")).default;
} catch {
throw new Error("better-sqlite3 is not installed. Run npm install before using setup.");
return { Database: (await import("better-sqlite3")).default };
} catch (error) {
return { error };
}
}
// #7586: unlike the real server (src/lib/db/adapters/driverFactory.ts::tryOpenSync),
// this CLI helper historically had NO fallback beyond better-sqlite3 — so on any
// machine where better-sqlite3's native binary is unavailable (Windows without a
// prebuilt addon, etc.), every `omniroute doctor` DB check reported a false FAIL
// even when the actual server was healthy via its own (correct) driver cascade.
// Reuse that same cascade here instead of re-deriving it.
async function openWithSyncDriverFallback(dbPath, options, importError) {
try {
const { tryOpenSync } = await import("../../src/lib/db/adapters/driverFactory.ts");
const adapter = tryOpenSync(dbPath, options);
if (adapter) {
return adapter;
}
} catch {
// fall through to the original better-sqlite3 error below
}
throw createSqliteNativeError(importError);
}
function openBunSqlite(Database, dbPath, options) {
const raw = new Database(dbPath, options);
const prepare = (sql) => {
@@ -91,19 +110,25 @@ export function createSqliteNativeError(error) {
}
async function openSqliteDatabase(dbPath, options = {}) {
const Database = await loadSqlite();
const loaded = await loadSqlite();
if (process.versions.bun) {
if (options.fileMustExist && !fs.existsSync(dbPath)) {
throw new Error(`SQLite file does not exist: ${dbPath}`);
}
options = options.readonly
const bunOptions = options.readonly
? { readonly: true }
: { readwrite: true, create: options.fileMustExist !== true };
try {
return openBunSqlite(loaded.Database, dbPath, bunOptions);
} catch (error) {
throw createSqliteNativeError(error);
}
}
if (loaded.error) {
return openWithSyncDriverFallback(dbPath, options, loaded.error);
}
try {
return process.versions.bun
? openBunSqlite(Database, dbPath, options)
: new Database(dbPath, options);
return new loaded.Database(dbPath, options);
} catch (error) {
throw createSqliteNativeError(error);
}

View File

@@ -0,0 +1 @@
- refactor(sse): classify SSE critical-path empty catches + add CONTRIBUTING convention (#8142)

View File

@@ -0,0 +1 @@
- feat(db): persist caller session tag into call_logs for per-session cost attribution (#8249)

View File

@@ -0,0 +1 @@
- feat(api): quota-aware fallback routing for web-fetch providers (#8297)

View File

@@ -0,0 +1 @@
- feat(providers): map upstream reasoning-level metadata in openai-compatible discovery (#8347)

View File

@@ -0,0 +1 @@
- **feat(adobe-firefly):** reference-image attach for generate + OpenAI `/v1/images/edits` support (follow-up to #8006). Uploads sources to Firefly storage (`POST /v2/storage/image`), then submits `referenceBlobs` on 3P generate-async (nano multi-ref `usage:general`; gpt-image `usage:subject`). Wire matches live `firefly.adobe.com` captures. Also routes built-in edits to the same path (up to 4 refs).

View File

@@ -0,0 +1 @@
- fix(providers): expose a base-URL override for Kimi/Moonshot so CN-region API keys (issued on platform.kimi.com / moonshot.cn) can be pointed at api.moonshot.cn instead of being rejected by the international host (#7447)

View File

@@ -0,0 +1 @@
- fix(sse): stop stream readiness from treating a choices-less mid-stream error frame as a successful stream, so combo can fail over instead of returning zero `choices` (#7503)

View File

@@ -0,0 +1 @@
- fix(cli): fall back to the node:sqlite driver cascade in `bin/cli/sqlite.mjs` so `omniroute doctor` no longer reports a false "FAIL Database"/"FAIL Storage/encryption" on machines without a working better-sqlite3 native binary (#7586)

View File

@@ -0,0 +1 @@
- fix(api): expose Responses-API-format (OpenAI/Codex) chat models on every VS Code Ollama-compatible listing route, not just `/models` (#7587)

View File

@@ -0,0 +1 @@
- fix(backend): stop `buildClientRawRequest` deep-cloning the whole request body on every chat request (#7847) — every consumer of `clientRawRequest.body` is observability and keeps at most a bounded copy, so the unbounded clone retained ~41x more than anything used it (3.19 MiB vs 0.08 MiB on a 3.05 MiB / 729-message agent request). Also makes `cloneBoundedForLog` idempotent: arrays, objects and strings all exceeded their own bounds once the truncation marker was added, so re-bounding an already bounded payload silently dropped a further item and misreported the original length

View File

@@ -0,0 +1 @@
- fix(sse): copy the combo attempt body shallowly instead of deep-cloning it per target (#7847) — the deep clone cost 9.53 MiB at 3 targets and scaled linearly with the target count (31.78 MiB at 10) on a 3.05 MiB agent request, while the isolation it provided only ever needed to contain top-level scalar writes. Also fixes a real cross-target leak in round-robin, which copied the body only when the reasoning buffer changed `max_tokens` and otherwise shared the caller's object, so a Background Task Redirection on one target rewrote `body.model` for the next

View File

@@ -0,0 +1 @@
- fix(sse): estimate the combo fallback-compression trigger from the request object instead of `JSON.stringify(...)` (#7847) — the string path charged an inline base64 image as if every character were prose (~50k tokens instead of ~1.2k on a 200 KB image), falsely tripping compression on requests nowhere near the context window; the same over-count #8368/#8401 fixed elsewhere. Adds `jsonLength()`, an exact serialized-length walker (property-tested against `JSON.stringify`), and uses it for the readiness-timeout and token estimates so a multi-megabyte body is no longer materialized as a string just to be measured

View File

@@ -0,0 +1 @@
- fix(providers): route noauth opencode-zen connections through their assigned proxy

View File

@@ -0,0 +1 @@
- fix(providers): repoint the zai-web executor at chat.z.ai's current v2 chat-completions endpoint, fixing model-independent 404s (#8014)

View File

@@ -0,0 +1 @@
- **fix(providers):** path-shaped multimodal model ids (e.g. `cp/cline-pass/kimi-k3`) resolve native vision via leaf/registry metadata instead of triggering Vision Bridge ([#8032](https://github.com/diegosouzapw/OmniRoute/issues/8032)) — thanks @Prudhvivuda

View File

@@ -0,0 +1 @@
- fix(translator): set `status: "completed"` on translated OpenAI Responses `input` items so strict Responses-compatible upstreams stop rejecting them with 400 MissingParameter input.status (#8083)

View File

@@ -0,0 +1 @@
- fix(providers): classify HTTP 400 model-unavailable as MODEL_NOT_FOUND so Antigravity Pro fallback locks out the deprecated model

View File

@@ -0,0 +1 @@
- fix(providers): carve cookie-auth providers out of terminal 401 'expired' classification so one 401 cooldowns instead of killing the connection

View File

@@ -0,0 +1 @@
- fix(providers): classify per-model-quota 403 and DEGRADED 400 as model-unhealthy in checkFallbackError (#8247, #8248)

View File

@@ -0,0 +1 @@
- **fix(providers):** reconcile Kimi K3 vision metadata when synced `attachment=false` contradicts image/video `modalities_input`, so `supportsVision`, `attachment`, and exposed modalities agree ([#8250](https://github.com/diegosouzapw/OmniRoute/issues/8250)) — thanks @Prudhvivuda

View File

@@ -0,0 +1 @@
- fix(api): fold namespace into the flattened Chat tool name so cross-namespace leaves do not collide

View File

@@ -0,0 +1 @@
- **fix(runtime):** sanitize Muse Spark fetch failures before logging and resolve the sql.js WASM asset without the unexported package metadata subpath ([#8298](https://github.com/diegosouzapw/OmniRoute/pull/8298)) — thanks @backryun

View File

@@ -0,0 +1 @@
- **fix(sse):** Combo middleware preserves OpenAI Responses request bodies and maps combo system overrides to `instructions`, preventing Responses-native fallbacks from receiving an unsupported `messages` parameter. ([#8310](https://github.com/diegosouzapw/OmniRoute/pull/8310)) — thanks @ridho9

View File

@@ -0,0 +1 @@
- fix(api): accept the current compatible-provider connection id scheme in the models test route (#8326)

View File

@@ -0,0 +1 @@
- fix(api): stop leaking the internal provider UUID in /v1/models and honor the configured prefix (#8327)

View File

@@ -0,0 +1 @@
- fix(dashboard): show custom provider_nodes providers in the Topology view instead of only AI_PROVIDERS (#8328)

View File

@@ -0,0 +1 @@
- fix(api): stop the 2000-token safety buffer from inflating usage.prompt_tokens in the client response (#8331)

View File

@@ -0,0 +1 @@
- fix(backend): keep combo routing from dispatching image requests to text-only targets (#8332)

View File

@@ -0,0 +1 @@
- fix(sse): strip third-party-agent signals from the Hermes system prompt that trigger Anthropic 400 extra-usage (#8350)

View File

@@ -0,0 +1 @@
- **fix(i18n):** Restore brand/model proper nouns (Claude, OpenAI, Anthropic, Gemini, MiniMax, etc.) in zh-CN and zh-TW — replace Chinese phonetic/translation forms (克劳德/打开Ai/人择/双子座) with original English, unify "provider" translation to "供应商/供應商", and apply zh-TW localized terminology (網路/設定/檔案/新增/啟用/搜尋/儲存) instead of mainland defaults (#8355 — thanks @ikelvingo).

View File

@@ -0,0 +1 @@
- fix(api): estimate inline base64 image tokens instead of counting the data URL as text so it does not falsely exceed the context window (#8368)

View File

@@ -0,0 +1 @@
- fix(backend): stop prompt-cache affinity from silently reordering an explicit priority combo across models (#8370)

View File

@@ -0,0 +1 @@
- fix(api): accept a missing status query-param on GET /api/plugins instead of rejecting null with Invalid status value (#8374)

View File

@@ -0,0 +1 @@
- fix(resilience): treat an unreachable-proxy ECONNREFUSED as a circuit-breaker event so combo fails over instead of hitting the 503 max-retry limit (#8376)

View File

@@ -0,0 +1 @@
- fix(backend): make disabling the global per-key proxy toggle override existing per-key proxy assignments (#8385)

View File

@@ -0,0 +1 @@
- fix(dashboard): persist compression engine detail settings (Headroom / session dedup / CCR) instead of dropping them on save (#8388)

View File

@@ -0,0 +1 @@
- fix(plugins): fire registered+active plugin hooks (onRequest/onResponse/onError) during proxying instead of never invoking them (#8395)

View File

@@ -0,0 +1 @@
- fix(resilience): cap the connection cooldown after a 429 burst so combo fallback is not blacked out past the real rate-limit window (#8396)

View File

@@ -0,0 +1 @@
- fix(api): reach synced model_capabilities rows for canonical provider ids that only appear as an alias in MODELS_DEV_PROVIDER_MAP, e.g. codex/claude (#8429)

View File

@@ -0,0 +1 @@
- fix(providers): stop marking a multi-quota-window provider exhausted when only some windows are depleted (LIMIT-200 snapshot eviction drops idle healthy windows) (#8431)

View File

@@ -0,0 +1 @@
- fix(backend): compute AgentBridge diagnose `dnsConfigured` per-agent instead of hard-coded to Antigravity hosts (#8466)

View File

@@ -0,0 +1 @@
- **fix(providers):** OpencodeExecutor honors Extra API Keys rotation via `resolveEffectiveKey` (empty primary + extras no longer omit Authorization) ([#8467](https://github.com/diegosouzapw/OmniRoute/issues/8467)) — thanks @Prudhvivuda

View File

@@ -0,0 +1 @@
- fix(sse): stop combo's "all targets failed" response from attaching one target's retry-after window to an unrelated target's error message (#8486)

View File

@@ -0,0 +1 @@
- fix(providers): persist a runtime-discovered Antigravity projectId back onto the connection so it survives token refreshes and restarts instead of being rediscovered or lost (#8491)

View File

@@ -0,0 +1 @@
- fix(cli): `backup create` / `backup auto enable` — option shadowing by the parent `backup` command removed; all flags (`--cloud`, `--encrypt`, `--retention`, `--name`, `--exclude`, `--key-file`) now reach the subcommand handler with their actual values instead of being silently discarded

View File

@@ -0,0 +1 @@
- fix(api): surface a non-blocking warning + startup scan when a combo name shadows a real model id, instead of silently routing with zero signal (#8530)

View File

@@ -0,0 +1 @@
- **fix(kiro):** support profileless Builder ID quota, preserve CLI auth identity, stabilize social OAuth polling, and use the live model catalog ([#8565](https://github.com/diegosouzapw/OmniRoute/pull/8565)) — thanks @nguyenha935

View File

@@ -0,0 +1 @@
- **chore(quality):** rebaseline complexity (2130→2183) and cognitive-complexity (951→968) across the v3.8.49 `/merge-prs` queue-drain. The first step (→2169/→956) cleared inherited base drift measured on the pristine release tip (the PR→release fast-path never ratchets these). The second step (→2183/→968) absorbs the aggregate own-growth of the 41-PR merge-train (each PR under-ceiling individually; the combined batch adds +14/+12). Owner-approved (2026-07-25); structural shrink tracked in [#3501](https://github.com/diegosouzapw/OmniRoute/issues/3501).

View File

@@ -0,0 +1 @@
- **chore(combo):** extract 8 pure error predicates and quota status helpers (`clampPercent`, `quotaRemainingPercentFromQuota`, `normalizeConnectionStatus`, `hasFutureRateLimitUntil`, `getConnectionStatusQuotaCutoffReason`, `isContextOverflow400`, `isParamValidation400`, `isModelScoped400`) from `open-sse/services/combo.ts` into `open-sse/services/combo/comboPredicates.ts` — pure move, zero behavior change; `combo.ts` shrinks from 3,651 to 3,554 lines while maintaining backward-compatible re-exports.

View File

@@ -0,0 +1 @@
- chore(validation): decompose `src/lib/providers/validation.ts` (→ 442 lines) by extracting the web-cookie, kiro and specialty inline validators into `validation/*` leaves — behavior-preserving move; the specialty validators that captured `isLocal` from the enclosing closure now take it as an explicit parameter, with the host dispatcher passing it at each call site

View File

@@ -0,0 +1 @@
- chore(token-refresh): decompose `open-sse/services/tokenRefresh.ts` (999 → 724 lines) by extracting the rotation-map, CAS guard and circuit-breaker refresh logic into `tokenRefresh/*` leaves — behavior-preserving move; `tokenRefresh.ts` still re-exports the moved symbols so the public surface is unchanged

View File

@@ -0,0 +1 @@
- chore(usage): decompose `open-sse/services/usage.ts` (999 → 253 lines) by extracting the crof, nanogpt, qoder, opencode, deepseek, bailian, vertex, xiaomi-mimo, xai and github usage fetchers into `usage/*` leaves — behavior-preserving move, the file is now a thin provider→fetcher dispatcher and the public import surface is unchanged

View File

@@ -0,0 +1 @@
- chore(perf): add `npm run bench:heap-body` — a deterministic benchmark that attributes retained V8 heap to each request-body copy on the chat path (entry log clone, combo per-target clone, token-estimation string), reproducing the #7847 incident shape (3.05 MiB / 729 messages / 86 tools) so the clone-amplification work can be justified and regression-guarded with numbers instead of intuition

View File

@@ -0,0 +1 @@
- chore(ci): resync the stale `no-explicit-any` suppression count for `tests/unit/proxy-registry.test.ts` (frozen at 55, actual 54 since #8447) — ESLint was failing the whole run with "There are suppressions left that do not occur anymore", turning `npm run lint` red on `release/v3.8.49` for every PR branched off it

View File

@@ -1,7 +1,8 @@
{
"_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.",
"_rebaseline_2026_07_20_owner_night_drain": "Owner-approved (chat, 2026-07-20 ~00:50): 2072->2130. The day's 17 merged PRs consumed the entire slack (tip at 2069/2072); queue PRs #6973(+4)/#7662(+2)/#7719(+1) plus the #7744/#7779 reworks were collectively blocked. Owner chose a wide margin for the remainder of the v3.8.49 cycle instead of per-PR extraction.",
"count": 2130,
"_rebaseline_2026_07_25b_v3849_mergetrain_owngrowth": "Owner-approved (chat, 2026-07-25): 2169->2183 (+14). v3.8.49 /merge-prs 41-PR merge-train aggregate own-growth: measured 2183 on the combined boarded tree (tip ac15014ca7) vs 2169 on the pristine release tip. Each boarded PR sits under the ceiling individually, but the combined batch adds +14 (new branches in #8378 chatCore contextLimit / #8432 cursor native_todo / #8476 combo input-bound / #8526 combo select-all modals / etc — the pre-screen-flagged complexity-growth set). Same merge-burst-inherited-drift class as the notes below; owner chose absorbing the ceiling over per-PR helper-extraction churn. Structural shrink stays debt (#3501); tighten via --update next cycle.",
"_rebaseline_2026_07_25_v3849_mergequeue_drain": "Owner-approved (chat, 2026-07-25): 2130->2169 (+39). v3.8.49 /merge-prs queue-drain: the cycle's merge burst (the 8 base-red slices + owner PRs + parallel-session merges #8500-8508) accrued inherited cyclomatic drift the fast-path PR->release never ratchets (check:complexity does not run on PR->release). Measured 2169 on the pristine release tip 4053e2314a alone (BEFORE any queue PR boards) — so the entire +39 is base drift already on the tip, not any queued PR's own growth. Every merge-ready PR in the queue was tripping Fast Quality Gates on this shared base-red. Owner approved raising the ceiling to the measured tip value so the ~34-PR merge-train lands without per-PR helper-extraction churn. Structural shrink stays debt (#3501); tighten via --update next cycle.",
"count": 2183,
"_rebaseline_2026_07_19_v3849_fix_sweep_cluster": "2059->2072 (owner-approved, 2026-07-19). /fix-prs validation-train sweep: a cluster of otherwise-clean contributor PRs (#6973/#7683/#7662/#7672/#7633/#7767, each +1/+2 cyclomatic own-growth from new provider/auth/combo branches) collectively pushed the count from tip 2056 to 2068. Individually all but #6973 sit under the old 2059 baseline; combined they exceed it. The tip had only 3 units of slack (2056 vs 2059), so every new-feature PR was tripping the ratchet (this was the 4th such block of the day after #7695/#7747/#7768). Owner approved raising the ceiling to 2072 = combined-cluster 2068 + 4 units headroom, so the cluster lands without per-PR helper-extraction churn and near-term feature PRs have breathing room. Measured 2068 on the 9-PR combined probe tree. Structural shrink stays debt (#3501); tighten via --update next cycle.",
"_rebaseline_2026_07_18_pr7360_quota_visibility_resync": "2058->2059 (+1 vs recorded ceiling; measured 2056 fresh on release tip cab9e5f0c alone, so this ceiling still carries 2 units of un-banked slack from prior shrinkage — real regression from this merge is 2056->2059, +3). PR #7360 (JxnLexn) release-resync: merging origin/release/v3.8.49 to resolve the 3-file conflict (ConnectionRow.tsx/ConnectionsListPanel.tsx/useProviderConnections.ts) unions two already-compliant features in the same already-oversized god-component: release's confirm-delete-account wiring (#7361) and this PR's per-connection quota-visibility wiring. Diffed release-tip-only vs merged violation lists (scripts dumped via getComplexityEslintReport): most entries are the SAME pre-existing violations shifted a few lines (ConnectionRow/getStatusPresentation/inferErrorType — no count change) or marginally bigger (ConnectionRow function complexity 85->86, ConnectionsListPanel function 498->510 lines) from the two ConnectionRow call sites each gaining both PRs' multi-line JSX props. The 2 genuinely NEW crossings are the 'no tag' and 'tagged groups' .map() render callbacks in ConnectionsListPanel.tsx (83 and 85 lines, was <=80 on both parents individually) tipping over 80 lines specifically because both PRs' props land on the same call sites. No new logic was written during the resync itself (only import-statement unions); the growth is inherent to combining the two already-reviewed feature branches. Structural shrink tracked in #3501. Tighten via --update next cycle (true floor is 2056, not 2058).",
"_rebaseline_2026_07_17_v3849_ownerprs_providers": "2056->2058 (+2). v3.8.49 owner-PR merge campaign own-growth: the new provider handlers/dispatch branches merged this cycle (freetheai/felo/notion/segmind/deepinfra/novita/msdesigner image+video handlers, each adding a format-dispatch guard) pushed cyclomatic violations 2056->2058. Fast-gates PR->release do not run the complexity ratchet, so this surfaced only on re-sync. Spread across the new leaf handlers (not a single extractable function); measured on the release tip. Structural shrink tracked in #3501.",

View File

@@ -4,11 +4,6 @@
"count": 1
}
},
"open-sse/executors/claudeIdentity.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 2
}
},
"open-sse/executors/cliproxyapi.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 1
@@ -415,11 +410,6 @@
"count": 1
}
},
"src/shared/components/KiroSocialOAuthModal.tsx": {
"react-hooks/exhaustive-deps": {
"count": 1
}
},
"src/shared/components/LanguageSelector.tsx": {
"@next/next/no-img-element": {
"count": 1
@@ -890,11 +880,6 @@
"count": 8
}
},
"tests/unit/claude-to-openai-think-close-5123.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 2
}
},
"tests/unit/cli-a2a-invoke-commands.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 16
@@ -1947,7 +1932,7 @@
},
"tests/unit/proxy-registry.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 55
"count": 54
}
},
"tests/unit/proxy-resolution-status-filter.test.ts": {
@@ -2267,7 +2252,7 @@
},
"tests/unit/translator-claude-to-gemini.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 14
"count": 17
}
},
"tests/unit/translator-claude-to-openai.test.ts": {
@@ -2287,7 +2272,7 @@
},
"tests/unit/translator-openai-to-gemini.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 68
"count": 74
}
},
"tests/unit/translator-openai-to-kiro.test.ts": {

View File

@@ -1,4 +1,5 @@
{
"_rebaseline_2026_07_25_8499_ts7_result_union_predicates": "PR #8499 (backryun, chore/ts7-types-executor-scattered) own growth: muse-spark-web.ts 1396->1405 (+9, irreducible). Under this workspace's `strictNullChecks: false`, the boolean-literal discriminant on `GraphqlResult` (`{ ok: true } | { ok: false; error: string }`) narrows the positive `.ok===true` branch but leaves `!result.ok` at the full union under TS7, making `.error` unreachable to the checker at the two call sites (warmup, mode-switch). Fixed by adding a single `isGraphqlFailure()` type-predicate helper (doc comment + 3-line body) reused at both call sites instead of duplicating the predicate inline — not extractable to a shared module without splitting a single-file executor's local narrowing helper out of its own file. Covered by the existing muse-spark-web executor test suite (no behavior change, pure narrowing fix).",
"_rebaseline_2026_07_22_8131_windowshide_cloudflared_spawn": "PR #8167 (Dingding-leo, fix/windows-hide-child-process, #8131) own growth: src/lib/cloudflaredTunnel.ts 934->935 (+1, irreducible call-site wiring — the single `windowsHide: true` option added to the existing cloudflared spawn() options object so no transient conhost.exe/cmd console window flashes open on Windows). Covered by the pre-merge-fix regression test tests/unit/windows-hide-child-process-spawns-8131.test.ts (added for the two additional spawn() sites the PR missed: ServiceSupervisor.ts, versionManager/processManager.ts) plus the windowsHide assertion added to tests/unit/services/installers/runNpm-shell-5379.test.ts (installers/utils.ts buildNpmExecOptions).",
"_rebaseline_2026_07_22_8006_adobe_firefly_media_provider": "PR #8006 (artickc, feat/adobe-firefly-media) own growth: adds Adobe Firefly as a media-only (image + video) provider — unofficial IMS/cookie-session bridge for firefly.adobe.com covering IMS cookie->access_token exchange, discovery-catalog fallback, credits/balance usage, and submit+poll dispatch for both image (nano-banana/gpt-image families) and video (Sora 2/Veo 3.1/Kling 3.0) generation, with 408-under-load retry handling. New leaf open-sse/services/adobeFireflyClient.ts frozen at 1958 (>>cap 800) — a single self-contained upstream client (mirrors the qoderCli.ts precedent for a new provider client that is legitimately large on day one: IMS auth, cookie/JWT normalization, payload builders for 2 media types x multiple model families, SSE-less submit/poll state machine, error sanitization); not extractable without scattering a single upstream integration across artificial module boundaries mid-PR. open-sse/config/imageRegistry.ts (existing, previously under cap) grows 800->821 (+21, the new adobe-firefly IMAGE_PROVIDERS entry + models list, additive registry data at the existing registry chokepoint). src/lib/usage/providerLimits.ts 1000->1003 (+3, adobe-firefly/firefly added to the existing apikey-usage-fetcher allowlist, irreducible call-site wiring mirroring the sibling #7994 PromptQL/HyperAgent entries in the same PR group). Covered by tests/unit/adobe-firefly.test.ts (35/35). Structural shrink tracked in #3501.",
"_rebaseline_2026_07_22_7994_hyperagent_web_provider": "PR #7994 (artickc, feat/hyperagent-web) own growth: adds HyperAgent (hyperagent.com) as a new unofficial web-cookie chat provider, reverse-engineered from live SPA captures (thread/session SSE flow, credits/usage endpoint). New leaf open-sse/executors/hyperagent.ts frozen at 937 (>cap 800) — single self-contained executor covering cookie auth, SSE parsing (text/session_start/session_end/done events), and a sticky thread/session cache for multi-turn continuity; not extractable without splitting the executor mid-request-flow (mirrors the sseParser.ts/muse-spark-web.ts precedent for new provider executors that exceed cap on day one). src/lib/usage/providerLimits.ts 1000->1003 (+3, irreducible call-site wiring adding hyperagent/ha to the existing USAGE_FETCHER_PROVIDERS-style allowlist at the chokepoint other web-cookie providers already extend). Covered by tests/unit/executor-hyperagent.test.ts (16/16). Structural shrink tracked in #3501.",
@@ -175,7 +176,7 @@
"open-sse/executors/duckduckgo-web.ts": 925,
"open-sse/executors/grok-web.ts": 1873,
"open-sse/executors/hyperagent.ts": 937,
"open-sse/executors/muse-spark-web.ts": 1394,
"open-sse/executors/muse-spark-web.ts": 1405,
"open-sse/executors/perplexity-web.ts": 1032,
"open-sse/handlers/audioSpeech.ts": 1061,
"open-sse/handlers/chatCore.ts": 5125,
@@ -186,15 +187,18 @@
"open-sse/handlers/videoGeneration.ts": 1275,
"_rebaseline_2026_07_22_8010_codex_responses_engine": "PR #8010 (@JxnLexn) own growth: open-sse/mcp-server/schemas/tools.ts 1497->1505 (+8 = threading the new \"codex-responses\" literal into the compressionConfigureInput strategy/autoTriggerMode Zod enums and setCompressionEngineInput engine enum, mirroring the existing rtk/omniglyph enum entries; no new tool). open-sse/services/compression/strategySelector.ts 1043->1054 (+11 = one new `if (mode === \"codex-responses\")` dispatch branch in runCompression that delegates 100% to the new codexResponsesEngine.apply, mirroring the existing rtk single-mode dispatch, plus threading config.codexResponsesConfig.preserveToolNames into the shared adaptBodyForCompression call at the 3 existing call sites). src/lib/db/compression.ts (untracked, new-file cap 800) 794->845 (+51 = normalizeCodexResponsesConfig, mirroring the existing normalizeRtkConfig normalizer, plus registering \"codex-responses\" in the COMPRESSION_MODES/STACKED_PIPELINE_ENGINE_IDS/SINGLE_MODE_ENGINE sets and the getCompressionSettings load/save switch) — added to the baseline at its current size. All three are cohesive dispatch/normalizer wiring at existing chokepoints (mirroring the prior compression-mode rebaselines #6534/#6556), not extractable without hiding the mode-dispatch boundary. Covered by tests/unit/compression/codex-responses.test.ts (6) + omniglyph-registries.test.ts/types.test.ts (22, updated for the new mode).",
"_rebaseline_2026_07_22_8034_compression_exclusions_persistence": "#8034 (compression exclusions) own growth: src/lib/db/compression.ts 845->850 (+5 = threading the new compressionExclusions field through the existing getCompressionSettings/saveCompressionSettings load/save switch over the shared key_value compression namespace — no new table, no raw SQL). Mirrors the prior compression-field rebaselines (#8010 codex-responses normalizer at the same chokepoint); the load/save switch is a single dispatch boundary, not extractable without hiding it. Covered by the PR's 8 node:test + 3 vitest cases.",
"src/lib/db/compression.ts": 866,
"_rebaseline_2026_07_24_8388_compression_detail_persist": "#8388 (compression engine DETAIL settings — Headroom/session-dedup/CCR — dropped on save) own growth: src/lib/db/compression.ts 866->872 (+6 = irreducible call-site wiring at the existing getCompressionSettings/updateCompressionSettings chokepoint: one import line, one `...buildDetailConfigDefaults()` spread in the seed config, and one `case \"sessionDedup\": case \"ccr\": applyDetailConfigUpdate(config, key, parsed); break;` load-switch case, mirroring the existing headroom/#8056 case immediately above it). The actual normalizer logic (normalizeSessionDedupConfig/normalizeCcrConfig, matching SESSION_DEDUP_SCHEMA/CCR_SCHEMA bounds) was EXTRACTED into a new leaf src/lib/db/compressionDetailNormalizers.ts (well under cap) so this frozen file only carries the minimal dispatch wiring. Covered by tests/unit/8388-compression-detail-persist.test.ts (schema-accept + full DB save->reload round-trip for both new sub-objects, plus a no-regression assertion on the existing headroom round-trip).",
"src/lib/db/compression.ts": 872,
"open-sse/mcp-server/schemas/tools.ts": 1505,
"open-sse/mcp-server/server.ts": 1555,
"open-sse/mcp-server/tools/advancedTools.ts": 1120,
"_rebaseline_2026_06_27_5193_antigravity_basered": "Base-red (pre-existing release drift, fast-gate PR->release skips check:file-size): accountFallback.ts 1773->1777 and src/app/api/providers/[id]/test/route.ts 924->940 were already over their frozen caps on release/v3.8.39 independent of any antigravity change. Owner chose to rebaseline (keep the documented issue-reference comments #1846/#1449/#347 etc.) rather than accept the contributor comment-stripping in #5200/#5198. Reverted #5200 to restore the comments; bumped these two frozen caps to the actual base sizes. No logic change.",
"_rebaseline_2026_07_22_8213_gemini_tpm_quota_cooldown_wait": "PR #8213 (hartmark, fix/gemini-tpm-quota-cooldown-wait) own growth: open-sse/services/accountFallback.ts 1857->1932 on the merged tip (release 1892 incl #8050 +35, plus this PR own growth +40); measured against the PR own merge-base was 1857->1898 (+41 — the release tip separately carries an unrelated +34 from #8050's antigravity 404 model-not-found lockout scoping, which this PR's branch does not include and this entry does not cover). Own growth is the Gemini TPM-ceiling classification + cooldown-wait wiring feeding into the combo cooldown-wait state machine (rate-limit wedge recovery) introduced by this PR's commit series. Irreducible additions at the existing account-fallback/model-lockout chokepoint. Covered by the PR's own gemini-rate-limit-tracker and TPM-ceiling benchmark test additions.",
"_rebaseline_2026_07_23_8252_combo_400_advance": "#8252 (@RaviTharuma) own growth: accountFallback.ts 1932->1940 (+8) + combo.ts 3604->3630 (+26) — advance combo on model-scoped 400s wrapped as invalid/Bad-Request. Irreducible wiring at existing account-fallback + combo dispatch chokepoints. Covered by combo-model-scoped-400-advance.test.ts.",
"open-sse/services/accountFallback.ts": 1940,
"open-sse/services/adobeFireflyClient.ts": 1958,
"_rebaseline_2026_07_23_8247_8248_model_unhealthy": "#8247+#8248 own growth: accountFallback.ts 1940->1941 (+1, irreducible import statement only — the substantive #8248 DEGRADED-pattern classifier was extracted into open-sse/config/errorConfig.ts, which has ample headroom, instead of growing this frozen file; #8247's fix is a single existing-line condition change, net zero lines). Scoping the credits-exhausted 403/429 branch to isCompatibleProvider() (per-model-quota openai/anthropic-compatible-* nicknames) so it stays model-scoped instead of terminalling the whole connection, and classifying NVIDIA NIM 'Function ... DEGRADED' 400 bodies as model-access-denied instead of a raw passthrough 400. Covered by tests/unit/8247-accountfallback-model-unhealthy.test.ts and tests/unit/8248-accountfallback-nvidia-degraded.test.ts.",
"open-sse/services/accountFallback.ts": 1966,
"open-sse/services/adobeFireflyClient.ts": 2317,
"_rebaseline_2026_07_25_adobe_firefly_reference_images": "Follow-up to #8006: storage upload + referenceBlobs for image/video and /v1/images/edits dispatch. adobeFireflyClient.ts 1958->2317 (+upload helpers, extract sources, resolve blob ids). Note: 2317 not 2316 — check-file-size.mjs counts LOC via split(\"\\n\").length (counts the trailing-newline empty element), which is 1 higher than `wc -l` on a file ending in \\n; the PR's original entry (2316) was measured with wc -l and undercounted by 1 against the actual gate.",
"open-sse/services/batchProcessor.ts": 915,
"open-sse/services/browserBackedChat.ts": 850,
"open-sse/services/claudeCodeCompatible.ts": 1202,
@@ -203,7 +207,8 @@
"_rebaseline_2026_06_24_quota_share_strategy": "Dedicated quota-share strategy (Phase 3 #9): combo.ts 3180->3190 (+10 = one new `else if (strategy === \"quota-share\")` dispatch branch in handleComboChat that delegates 100% to selectQuotaShareTarget + its log line, plus the import). All the new logic lives OUT of the god-file in two new leaves under open-sse/services/combo/: quotaShareInflight.ts (in-flight counter with TTL/lease, ~150 LOC <cap) and quotaShareStrategy.ts (per-model bucket gating via isBucketSaturated + DRR proportional to weight + P2C over in-flight, ~240 LOC <cap). Only the dispatch wiring is irreducible at the existing combo strategy chokepoint (mirrors the headroom/reset-aware/reset-window/context-optimized branches); not extractable without hiding the call site. ZERO existing strategy cases were modified — only this branch was added, and the qtSd/ combos switched from fill-first to quota-share in src/lib/quota/quotaCombos.ts. Covered by tests/unit/quota-share-strategy.test.ts (gating, DRR fairness, P2C in-flight, fail-open, activation). Structural shrink of combo.ts tracked in #3501.",
"_rebaseline_2026_06_24_task_aware_routing": "Task-aware routing strategy (port PR #2045, OmniRoute #4945): combo.ts 3190->3225 (+35) = one new `else if (strategy === \"task-aware\")` dispatch branch delegating 100% to selectTaskAwareTarget + its imports/log lines. All scoring/classification logic lives OUT of the god-file in the new leaf open-sse/services/taskAwareRouting.ts (553 LOC <cap). Only the dispatch wiring is irreducible at the existing combo strategy chokepoint (mirrors quota-share/headroom/reset-aware branches). ZERO existing strategy cases modified. Covered by tests/unit/combo-task-aware.test.ts (35 tests). Structural shrink of combo.ts tracked in #3501.",
"_rebaseline_2026_07_22_8213_combo_cooldown_wait_recording": "PR #8213 (hartmark, fix/gemini-tpm-quota-cooldown-wait) own growth: open-sse/services/combo.ts 3548->3604 (+56, entirely this PR's diff — no other commit touched this file between the PR's merge-base and the release tip). Fixes combo cooldown-wait state recording so a bogus 503 is no longer crystallized when the cooldown-wait vars reset every setTry, adds an OpenAI-format SSE error frame path for combo-exhausted rejections (capturing request body + attempted models), and gives an abandoned per-target dispatch its own timeout instead of leaking a permanent 'pending' dashboard entry. Irreducible additions at the existing handleComboChat dispatch/retry chokepoint (mirrors the prior quota-share/headroom/task-aware strategy-branch precedents already frozen in this file). Covered by the PR's own combo-config + Gemini TPM-ceiling benchmark test additions.",
"open-sse/services/combo.ts": 3630,
"_rebaseline_2026_07_25_8476_combo_input_bound_homogeneous_scope": "PR #8476 (herjarsa, fix/8375-8459-combo-image-fixes, #8375) own growth: open-sse/services/combo.ts 3642->3679 (+37 net: +29 the PR's own isInputBoundFailure short-circuit for deterministic context_length_exceeded/context_window_exceeded failures, +8 a /green-prs pre-merge fix scoping that short-circuit to homogeneous remainders only — the shipped code fired unconditionally on ANY target, regressing the intentional heterogeneous-combo fallback #6637/isContextOverflow400 protects, exactly as flagged by this PR's own review evidence but never actually implemented in the branch). The fix compares orderedTargets[i+1..] modelStr against the failing target's modelStr at the existing executeTarget dispatch chokepoint (mirrors the sameProviderNext precedent a few lines below) — irreducible call-site wiring, not extractable without hiding the dispatch boundary. Covered by tests/unit/combo-input-bound-failure-8375.test.ts (homogeneous pool still short-circuits) and the new tests/unit/combo-input-bound-heterogeneous-8375.test.ts (heterogeneous combo now correctly falls through to the larger-context target).",
"open-sse/services/combo.ts": 3679,
"_rebaseline_2026_06_26_fidelity_gate_extraction": "Milestone-B fidelity-gate wiring residual: bodyToText+gateAdvance extracted to fidelityGateStep.ts (889->854, -35), but the StackOptions.fidelityGate field, the `const fidelityGate` reads at the two stacked-loop dispatch chokepoints, and the import of FidelityGateConfig are irreducible wiring that cannot leave strategySelector without an architectural refactor of the pre-existing stacked pipeline. Net: 889->854 (+6 vs the pre-Milestone-B frozen 848). Covered by tests/unit/compression/*.test.ts (940 pass).",
"_rebaseline_2026_06_28_5243_risk_gate_prepass": "PR #5243 (compression risk-gate pre-pass) own growth: open-sse/services/compression/strategySelector.ts 854->899 (+45). The three exported entry points (applyCompression/applyStackedCompression/applyStackedCompressionAsync) become thin wrappers over pure-extracted private bodies (runCompression/runStackedCompression/runStackedCompressionAsync) so the risk-gate mask->run->restore wrapper sits strictly OUTSIDE the per-step loop — a single universal integration point. The wrapper logic itself (resolveRiskGate/withRiskGate) lives in the new riskGate/strategyWrap.ts (<cap); the residual growth is the duplicated thin-wrapper signatures + the extracted bodies' dispatch boundary, guarded by a byte-identical parity test (riskGateIntegration). Default off (DEFAULT_COMPRESSION_CONFIG unchanged). Not extractable without hiding the dispatch boundary, mirroring prior compression rebaselines. Structural shrink tracked in #3501.",
"_rebaseline_2026_06_29_5286_memoization": "PR #5286 own growth: strategySelector.ts 899->960 (+61 = the opt-in result-memoization branches in applyCompression/applyCompressionAsync — principal+determinism gate, makeMemoKey lookup/store with model+supportsVision folded into the key, recompute-with-memo-off). Default off (memoizeCompressionResults), so zero behavior change. The memo helpers live in the leaf resultMemo.ts (<cap); the chokepoint wiring here is not extractable. Structural shrink of this hot-path file tracked in #3501.",
@@ -222,7 +227,8 @@
"open-sse/translator/response/gemini-to-openai.ts": 821,
"_rebaseline_2026_07_22_7936_namespace_roundtrip": "#7936 (@RCrushMe, Responses-Chat namespace round-trip identity seam) own growth: open-sse/translator/response/openai-responses.ts 1092->1125 (+33) and open-sse/utils/stream.ts 2814->2869 (+55) — threading the namespace-identity seam through the Responses↔Chat translation + stream paths so tool-call namespaces survive the round-trip. Cohesive translation/stream wiring at existing chokepoints, frozen at new size.",
"_rebaseline_2026_07_22_8210_openrouter_midstream_error": "PR #8210 (hartmark, fix/openrouter-midstream-error-surfacing) own growth: open-sse/translator/response/openai-responses.ts 1137->1163 (+26) measured on the merged tip (release 1137 + this PR own growth). Adds a single new branch inside openaiToOpenAIResponsesResponse() that detects an OpenRouter-style mid-stream aggregator error (HTTP 200 SSE chunk with empty choices + a top-level error object) and surfaces it as state.upstreamError instead of silently falling through to the no-op/awaitingTrailingUsage path, which previously masked the failure as a false empty-success completion and skipped combo fallback. Irreducible call-site addition at the existing chunk-dispatch chokepoint (mirrors the Gemini-to-OpenAI translator's #4177 precedent for the same class of upstream error surfacing). Note: this baseline entry does NOT cover the separate pre-existing +11 drift already on the release tip from #8081/#8162 (1125->1136, unrelated reasoning-placeholder-stripping fix merged after this PR branched) — that drift belongs to the maintainer's rebaseline, not this PR.",
"open-sse/translator/response/openai-responses.ts": 1163,
"_rebaseline_2026_07_24_responses_toolcalls_log_summary": "hartmark, fix/responses-tool-calls-log-summary own growth: open-sse/translator/response/openai-responses.ts 1163->1174 (+11). closeToolCall() now also writes the completed tool call into the shared state.toolCalls Map (already populated by the openai-to-claude / claude-to-openai / gemini-to-openai response translators) so stream.ts's completion-log summary builder (which reads state.toolCalls, not this translator's own funcCallIds/funcNames/funcArgsBuf bookkeeping) reports finish_reason \"tool_calls\" and message.tool_calls for openai->openai-responses translated streams instead of always logging \"stop\" with no tool_calls — the actual client-facing SSE events were already correct; only the persisted call-log summary was wrong. Irreducible call-site addition at the existing tool-call-close chokepoint. Covered by the new regression test in tests/unit/translator-resp-openai-responses.test.ts.",
"open-sse/translator/response/openai-responses.ts": 1174,
"open-sse/utils/cursorAgentProtobuf.ts": 1521,
"_rebaseline_2026_07_23_8143_empty_catch_logging": "#8143 (@chirag127) own growth: open-sse/utils/stream.ts 2869->2887 (+18) — replacing empty catch blocks in the SSE stream subsystem with console.debug logging (Rule #6 silent-swallow fix, issues #8138-#8142). Cohesive logging additions at the existing catch chokepoints, not extractable; frozen at new size. Covered by tests/unit/stream-handler-catch-logging-8143.test.ts.",
"open-sse/utils/stream.ts": 2887,
@@ -250,7 +256,7 @@
"src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderSettings.ts": 264,
"src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts": 1054,
"src/app/(dashboard)/dashboard/providers/components/onboarding/ProviderOnboardingWizard.tsx": 948,
"src/app/(dashboard)/dashboard/providers/page.tsx": 1927,
"src/app/(dashboard)/dashboard/providers/page.tsx": 1990,
"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": 903,
@@ -308,10 +314,10 @@
"_rebaseline_2026_07_09_6678_routing_strategy_9router": "#6678 (SeaXen) — 9router-parity Routing Strategy settings card + per-provider/combo sticky-round-robin override. Own growth: ProviderDetailPageClient.tsx 784->786 (single ProviderAccountRoutingCard mount + import), auth.ts 2448->2458 (providerStrategies override resolution: fallbackStrategy/stickyRoundRobinLimit per-provider cascade in getProviderCredentials). Both additive, zero unrelated refactor; new UI/logic lives in new files (ProviderAccountRoutingCard.tsx, RoutingStrategyCard.tsx, rrState.ts::resolveComboStickyRoundRobinLimit). chat.ts value below reflects the current release tip (grown by other concurrent PRs, e.g. #6640), not this PR own change.",
"_rebaseline_2026_07_22_8213_chat_abandoned_target_abort": "PR #8213 (hartmark, fix/gemini-tpm-quota-cooldown-wait) own growth: src/sse/handlers/chat.ts 1794->1860 (+66, measured against the PR's own merge-base — the release tip separately carries an unrelated -5 net shrink from #8013's antigravity callable-catalog alignment, which this PR's branch does not include and this entry does not cover). Adds resolveDispatchClientRawRequest(): merges a per-target modelAbortSignal into clientRawRequest.signal (via mergeAbortSignals) so a combo target abandoned by comboTargetTimeoutMs actually observes its own abort and reaches its cleanup path, instead of hanging forever inside withRateLimit/acquireAccountSemaphore and leaking a permanent 'pending' dashboard entry (live incident, log id 1784418258231-14961a). Also wires combo-exhausted rejection logging to capture request body + attempted models via the new rejectedRequestUsage helper. Irreducible additions at the existing chat dispatch chokepoint. Covered by the PR's own combo-config + integration test additions.",
"_rebaseline_2026_07_23_8127_grok_weekly_quota": "#8127 (@apoapostolov) own growth: src/sse/handlers/chat.ts 1861->1865 (+4) — weekly quota tracking for grok-web wires a quota-fetch hook at the existing dispatch chokepoint. Thin wiring mirroring adjacent provider-quota branches; not extractable. Covered by tests/unit/grok-quota-fetcher.test.ts.",
"src/sse/handlers/chat.ts": 1865,
"src/sse/handlers/chat.ts": 1866,
"_rebaseline_2026_07_20_7779_routingcombo_thread": "PR #7779 own growth: chatHelpers.ts 876->877 (+1, thread routingComboId into executeChatWithBreaker for compression-combo assignment). Frozen so it can only shrink.",
"src/sse/handlers/chatHelpers.ts": 878,
"src/sse/services/auth.ts": 2462,
"src/sse/services/auth.ts": 2486,
"open-sse/executors/default.ts": 890,
"open-sse/translator/request/openai-responses.ts": 902,
"open-sse/executors/kiro.ts": 944,
@@ -321,7 +327,7 @@
"open-sse/executors/huggingchat.ts": 813,
"_rebaseline_2026_07_01_v3843_release_5609": "Rebaseline v3.8.43 (PR #5609 release reconciliation). DRIFT dos 109 commits do ciclo: 8 god-files existentes cresceram (ApiManagerPageClient 2983->3017, combos/page 4594->4608, AddApiKeyModal 868->869, providerPageHelpers 974->996, chat.ts 1635->1647, auth.ts 2401->2403, batchProcessor 828->915, combo.ts 3368->3387) + 2 novos acima do cap (huggingchat.ts 813, tests web-cookie-providers-new 827) + 4 test files cresceram. Modularizacao deferida (blast-radius mid-release); congelado no estado atual p/ o proximo ciclo ratchetar daqui.",
"src/lib/providers/validation/webProvidersA.ts": 809,
"src/lib/tokenHealthCheck.ts": 832,
"src/lib/tokenHealthCheck.ts": 843,
"_rebaseline_2026_07_09_6587_kiro_api_key_auth": "PR #6587 (@strangersp) own growth for Kiro long-lived API-key auth, merged onto v3.8.47 tip: openai-to-kiro.ts 890->912 (+22, auth-header selection for API-key-vs-OAuth-token connections), providerLimits.ts 998->1000 (+2, API-key auth-type branch), translator-openai-to-kiro.test.ts 1234->1257 (+23), providers-page-utils.test.ts 1109->1107 (net -2 after merging with parallel release drift; connectionMatchesProviderCard api_key coverage added), provider-validation-specialty.test.ts 2856->2980 (+124 net after merge with parallel release drift; this PR also removed the file's `@typescript-eslint/no-explicit-any` eslint-suppression entry by fixing all `any` usages, adding typed replacements). Cohesive additive feature growth, well tested; not extractable without splitting the existing chokepoints mid-merge.",
"_rebaseline_2026_07_19_7787_ic2_localdb_reexports": "PR #7787 (IC2 raw connections cache + lazy-decrypt) own growth: localDb.ts 805->807 (gate units, +2). localDb.ts is the re-export-only layer (hard rule #2 — no logic); the PR adds 4 new db/readCache re-exports (touchConnectionLastUsed, getCachedRawProviderConnections, getCachedProviderConnectionById, getCachedProviderNodes) required by existing barrel importers. Irreducible for a re-export list; frozen so it can only shrink.",
"_rebaseline_2026_07_20_7819_autocandidateoverrides_reexport": "PR for #7819 (Level 1+2: read-only auto/* candidate transparency + per-API-key exclusions) own growth: localDb.ts 807->808 (+1). Adds a single `export * from \"./db/autoCandidateOverrides\"` barrel re-export (hard rule #2 — no logic) for the new DB module backing per-apiKey candidate exclusions. Irreducible for a re-export list; frozen so it can only shrink.",
@@ -338,6 +344,8 @@
},
"testCap": 800,
"testFrozen": {
"_rebaseline_2026_07_25_8510_adobe_firefly_reference_images_tests": "#8510 (artickc, feat/adobe-firefly-reference-images) own test growth: tests/unit/adobe-firefly.test.ts 711->871 (+159, entirely this PR's diff — new referenceBlobs upload/dispatch coverage for handleAdobeFireflyImageGeneration, resolveAdobeSourceImageIds, and the storage-upload wire contract). Route-level /v1/images/edits coverage (credentials/rate-limit/4-ref-cap branches added to route.ts) lives in the new tests/unit/8510-adobe-firefly-edits-route.test.ts instead of growing this file further.",
"tests/unit/adobe-firefly.test.ts": 871,
"_rebaseline_2026_07_09_6126_clinepass_dualauth": "#6126 (ClinePass dual-auth) own test growth: oauth-providers-config.test.ts 842->845 (+3: clinepass key/config/required-fields entries reusing the Cline WorkOS flow config, needed after registering clinepass in the oauth.ts PROVIDERS enum).",
"_rebaseline_2026_06_27_5193_antigravity_test": "#5193 own test growth: oauth-providers-config.test.ts 870->873 (+3: antigravity projectId assertion + 50ms tick for the now fire-and-forget onboarding, matching the no-PKCE/no-openid flow).",
"_rebaseline_2026_07_02_5928_base_red": "web-cookie-providers-new.test.ts 845->850: #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.",
@@ -384,7 +392,7 @@
"tests/unit/translator-friendly-test-bench.test.tsx": 848,
"tests/unit/translator-helper-branches.test.ts": 870,
"tests/unit/translator-openai-responses-req.test.ts": 1195,
"tests/unit/translator-openai-to-gemini.test.ts": 1553,
"tests/unit/translator-openai-to-gemini.test.ts": 1616,
"tests/unit/translator-openai-to-kiro.test.ts": 1257,
"tests/unit/translator-resp-gemini-to-openai.test.ts": 1234,
"tests/unit/usage-service-hardening.test.ts": 1503,
@@ -464,6 +472,9 @@
"_rebaseline_2026_07_18_basereds_test_realignment": "Base-red sweep own growth (post 102-PR campaign, full-suite realignment): tests/unit/combo-routing-engine.test.ts 3209->3243 (+34 = least-used tests now prime usage through real handleComboChat calls so recordComboRequest keys by the resolved executionKey exactly as production does — #7015 keying); tests/unit/db-migration-runner.test.ts 1491->1499 (+8 = withNonTestEnvironment now also strips node --test tokens from process.execArgv, matching the #7359 isAutomatedTestProcess widening); tests/unit/executor-default-base.test.ts 1523->1527 (+4 = 1M-beta assertion updated for claude-sonnet-4-6 GA #7129). All three are test-fidelity realignments, not extractable.",
"_rebaseline_2026_07_21_7930_pplx_quota_cooldown": "PR #7930 (@artickc) own growth, reconstructed against release/v3.8.49 base-drift: tests/unit/perplexity-web.test.ts 1192->1355 (+163 = two new regression cases — 'Live multi-step: reconstructs answer without status COMPLETED' proving RFC-6902 diff-patched plan_block goals now surface as reasoning_content the same as a materialized plan_block, and 'Advanced-model quota upsell with empty answer surfaces clear error' proving the new advanced_models_quota_low upsell_information detection maps to HTTP 429 + reset_seconds + Retry-After instead of a silent empty-content 502). Most of the PR's original 'multi-step empty content' claims were already independently fixed on release via a different mechanism (extractAnswerFromFinalText + longestMarkdownAnswer); only the two genuinely new, non-conflicting pieces (diff-block plan-goal extraction + quota cooldown) were ported. Covered by the two new tests; not extractable without splitting the whole executor test file.",
"_rebaseline_2026_07_22_v3849_ownGrowth_merge_batch": "OAuthModal(#7735 grok chooser), muse-spark-web(#7528 WS), combo.ts+combo-routing-engine.test(#7301 cooldown-retry) — pre-existing on tip; PricingTab(#7972), ComboDefaultsTab(#8008/#7973) — this train batch. Legitimate own-growth, owner-approved rebaseline.",
"_rebaseline_2026_07_23_v3849_merge_train_15": "Own-growth do merge-train de 15 PRs (2026-07-23), medido na tip combinada, release pura abaixo do baseline (auth.ts 2448, muse-spark 1393, translator-test 1523). auth.ts 2462->2475 (#8321 cookie-auth 401 cooldown-em-vez-de-terminal + #8324 noauth opencode-zen via proxy — wiring de classificação no chokepoint getProviderCredentials/markAccountUnavailable, não extraível), muse-spark-web.ts 1394->1396 (#8298 sanitizeErrorMessage runtime repairs isolados do #8177), tests/unit/translator-openai-to-gemini.test.ts 1553->1616 (#8312 cobertura do cap de thinking budget no path budget_tokens explícito). Owner-approved. Frozen; shrink estrutural em #3501.",
"_rebaseline_2026_07_22_providerLimits_webcookie_chain": "providerLimits.ts 1003->1005: own-growth from web-cookie provider usage-fetcher entries (#7994/#8006/#8027 chain) landing after the prior rebaseline.",
"_rebaseline_2026_07_22_8056_headroom_minrows": "#8056 (@RaviTharuma, persist Headroom minRows) own growth: src/lib/db/compression.ts 850->866 (+16 HeadroomConfig+DEFAULT_HEADROOM_CONFIG+normalize/store in get/updateCompressionSettings) and open-sse/services/compression/strategySelector.ts 1054->1060 (+6 merge settings.headroom into stacked stepConfig). Cohesive settings-persistence + stacked-merge wiring at existing chokepoints, frozen at new size."
"_rebaseline_2026_07_22_8056_headroom_minrows": "#8056 (@RaviTharuma, persist Headroom minRows) own growth: src/lib/db/compression.ts 850->866 (+16 HeadroomConfig+DEFAULT_HEADROOM_CONFIG+normalize/store in get/updateCompressionSettings) and open-sse/services/compression/strategySelector.ts 1054->1060 (+6 merge settings.headroom into stacked stepConfig). Cohesive settings-persistence + stacked-merge wiring at existing chokepoints, frozen at new size.",
"_rebaseline_2026_07_25_v3849_basered_filesize": "Base-red unblock (2026-07-25): check:file-size was failing on release/v3.8.49 at its own HEAD (36f8fd10), so the quality.yml fast-gates job was red for EVERY PR->release regardless of content — growth inherited from already-merged PRs, with no offending PR branch left to fix (same situation as _rebaseline_2026_07_02_5798_release_green). Prod frozen raised to the current base values: src/lib/tokenHealthCheck.ts 832->841, src/sse/handlers/chat.ts 1865->1866, src/sse/services/auth.ts 2475->2486, open-sse/services/accountFallback.ts 1941->1966, open-sse/services/combo.ts 3630->3642. accountFallback.ts was first frozen here at 1960 (the base value at 36f8fd10) and re-measured to 1966 at base tip 1cafd328c a few hours later — the same inherited drift this entry exists for, since check:file-size does not run on the PR->release fast path and so accrues unmeasured between release rebaselines. These files remain frozen and cannot grow further; any in-flight PR that adds lines to them (e.g. #8482 touches accountFallback.ts and combo.ts) bumps its own entry as usual. The release captain rebaseline-at-release supersedes this note.",
"_rebaseline_2026_07_25_v3849_basered_filesize_2": "Base-red unblock (2026-07-25, second pass): after _rebaseline_2026_07_25_v3849_basered_filesize (measured at 36f8fd10) two more already-merged PRs grew frozen files on release/v3.8.49, so check:file-size — and with it the whole Fast Quality Gates job — is red for EVERY PR->release again, with no offending PR branch left to fix. src/lib/tokenHealthCheck.ts 841->843 (#8426 4528fc455, excludes local CLI providers from expiration) and src/app/(dashboard)/dashboard/providers/page.tsx 1927->1990 (#8349 58ab8b1d2, scroll-position restore on provider-card back-navigation). Trust-but-verify: both values measured on the pristine release tip 30709255 with no working-tree changes. Same situation and remedy as _rebaseline_2026_07_02_5798_release_green. Structural reduction of providers/page.tsx stays tracked separately — it is a 1990-line page, not something to extract inside a base-repair PR."
}

View File

@@ -123,7 +123,9 @@
"_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": 951,
"value": 968,
"_rebaseline_2026_07_25b_v3849_mergetrain_owngrowth": "Owner-approved (chat, 2026-07-25): 956->968 (+12). v3.8.49 /merge-prs 41-PR merge-train aggregate own-growth: measured 968 on the combined boarded tree (tip ac15014ca7) vs 956 on the pristine release tip. The batch's new over-threshold functions come from the pre-screen-flagged complexity-growth set (#8378/#8432/#8476/#8526 etc); each PR is under-ceiling alone, the combined batch adds +12. Same merge-burst class as the notes below; owner chose ceiling-absorb over per-PR extraction. Structural shrink tracked in #3501; tighten via --update next cycle.",
"_rebaseline_2026_07_25_v3849_mergequeue_drain": "Owner-approved (chat, 2026-07-25): 951->956 (+5). v3.8.49 /merge-prs queue-drain: inherited cognitive-complexity drift from the cycle's merge burst (base-red slices + owner PRs + parallel-session merges #8500-8508); check:cognitive-complexity does not run on PR->release fast-gates, so it accrued unmeasured. Measured 956 on the pristine release tip 4053e2314a alone (BEFORE any queue PR boards) — the entire +5 is base drift already on the tip, reddening Fast Quality Gates for every merge-ready PR. Owner approved raising the ceiling to the measured tip value so the ~34-PR merge-train lands without per-PR extraction churn. Structural shrink tracked in #3501; tighten via --update next cycle.",
"_rebaseline_2026_07_10_gcf_v3_2": "885->888 (+3). PR feat/headroom-gcf-v3.2-nested-flattening: own growth from re-vendoring the GCF (Headroom) codec to spec v3.2 (nested flattening). The new over-threshold functions are the vendored v3.2 flatten/unflatten walk in open-sse/services/compression/engines/headroom/gcf/{generic,decode_generic}.ts. Imported third-party code kept byte-faithful to upstream gcf-typescript; measured 888 with the update vs 885 on the pristine origin/release/v3.8.47 tip. Guarded by tests/unit/compression/headroom-smartcrusher.test.ts (deep-nested case). Structural shrink belongs upstream in gcf.",
"_rebaseline_2026_07_12_v3847_mergetrain_burst": "885->890 (+5). v3.8.47 /merge-prs merge-train batch (23 merge-ready PRs) inherited drift: cognitive-complexity does NOT run on PR->release fast-gates, so incidental growth accrued unmeasured across the batch. Measured 890 on the combined merge-train tip (5d980352d) vs 885 on the pristine release tip 1b7a9150e. #6838 (headroom gcf codec re-vendor) accounts for +3 (its own baseline bump to 888, superseded here by this later 890 rebaseline which already covers it); the remaining +2 is parallel-batch drift across the other 22 PRs. Owner-approved rebaseline (merge-burst reconciliation, same class as the v3.8.46/v3.8.44 notes below). Tighten via --update next cycle.",
"_rebaseline_2026_07_09_6587_kiro_api_key_auth": "884->885 (+1). PR #6587 (@strangersp) own growth: open-sse/services/usage/kiro.ts gains ONE new over-threshold function — getKiroUsage grew from a single fetch to a 3-endpoint fallback chain (codewhisperer-get / codewhisperer-post / q-get) with per-attempt auth-header selection (tokentype: API_KEY vs Bearer-only), needed so usage/quota lookups work for the new long-lived-API-key auth path in addition to the existing OAuth path (measured: 0 violations on release tip -> 1 violation, complexity 33, at open-sse/services/usage/kiro.ts). Covered by tests/unit/kiro-iam-profilearn-usage.test.ts (tokentype header selection, friendly auth-expired/rejected-token messages). Cohesive multi-endpoint-fallback logic at an existing usage chokepoint; not extractable without splitting the fallback loop mid-merge. Structural shrink tracked in #3501.",

View File

@@ -36,6 +36,14 @@
"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/video-dashscope.test.ts": {
"replacement": "tests/unit/alibaba-video-media.test.ts",
"reason": "v3.8.49 #8266 reorganized the Alibaba video catalog: the flat wan2.7-t2v id moved out of the plain \"alibaba\" provider (which now only exposes the dated wan2.7-t2v-2026-06-12) and lives only under \"qwen-cloud\" today. All 6 tests in the deleted file built requests against alibaba/wan2.7-t2v, which the new allowlist now rejects with HTTP 400 (verified directly against VIDEO_PROVIDERS in open-sse/config/videoRegistry.ts before deletion). Coverage for this exact id already exists and was confirmed green pre-deletion in tests/unit/alibaba-video-media.test.ts (including an explicit \"Alibaba rejects video models outside its own allowlist\" case for alibaba/wan2.7-t2v) and tests/unit/qwen-cloud-video-media.test.ts (covers the same flat id under qwen-cloud/wan2.7-t2v). Verified legitimate, not masking."
},
"tests/unit/usage-analytics-route-extra.test.ts": {
"replacement": "tests/unit/usage-analytics-route.test.ts",
"reason": "v3.8.49 base-red reconcile: the file was a 100% duplicate — all 10 of its test names exist verbatim (comm -12 verified) among the 22 in tests/unit/usage-analytics-route.test.ts, created by #7700 before #7300 fixed the retention-cutoff fixture only in the main file (reads getUserDatabaseSettings().retention.usageHistory live instead of hardcoding 30d). The stale copy red-failed 'does not double-count raw and aggregated rows' (1 !== 2) against correct production behavior. Zero unique coverage lost; the maintained main file passes 22/22. Verified legitimate, not masking."
}
},
"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.",

90
docs/ROADMAP.md Normal file
View File

@@ -0,0 +1,90 @@
# OmniRoute Roadmap
> Version-gated, not date-gated: each milestone ships when its quality gates pass.
> Current line: **v3.8.x** (this branch). Last updated: 2026-07-23.
OmniRoute is heading from a monolithic router to a **modular AI platform**: a lightweight
core engine, a typed SDK, and everything else as installable modules and plugins. The path
runs through a stabilization rail (3.8.50 → 3.8.59), an LTS anchor (**3.9.0**), and the
modular **4.0**.
## The rail at a glance
```
3.8.50 ─ 3.8.54 PREPARE non-breaking structural prep (all PRs welcome)
3.8.55 ─ 3.8.59 VALIDATE stabilization (fixes / docs / i18n / providers only)
3.9.0 LTS stable/v3 branch · long-term support line
4.0.0-nightly/rc MODULAR core + SDK + modules + marketplace (develop branch)
4.0.0 GA latest switches to v4 · v3 stays supported as LTS
```
## Phase 1 — Preparation (3.8.50 → 3.8.54)
Non-breaking structural work that de-risks the modular split. Every version closes with a
mandatory quality-gate battery before new merges open.
| Version | Focus |
| --- | --- |
| 3.8.50 | CI safety net on release branches · dead-code cleanup · community-reported catalog/topology bug fixes · contributor "golden path" guide |
| 3.8.51 | Executor registry (in-place) · end-to-end provider-journey contract test becomes a CI gate · official scoped-test dev loop · CI lane consolidation (shared install/setup across gate jobs, #8084) |
| 3.8.52 | `combo.ts` decomposition · routing-strategy registry · unified model-catalog contract for `/v1/models` · one CI policy for PRs to `release/**` and `main` (#8084) |
| 3.8.53 | `chatCore.ts` decomposition · headless mode (`OMNIROUTE_HEADLESS=1`) · local candidate build/promote loop |
| 3.8.54 | Release infrastructure (dormant): channels, labels, PR templates, merge queue · full-regression authority moves to the merge queue once TIA shadow evidence clears (#8084) · public feature-freeze announcement |
## Phase 2 — Validation (3.8.55 → 3.8.59)
**External feature PRs pause here** (they get the `v4-feature` label and are re-targeted to
the v4 channel when it opens). Fixes, docs, i18n, and provider updates keep flowing.
| Version | Focus |
| --- | --- |
| 3.8.55 | Characterization tests for every extraction candidate · coupling re-measurement |
| 3.8.56 | Extended canary · performance baselines (heap, TTFB, build) |
| 3.8.57 | Security & compliance sweep · publish provenance (OIDC) rehearsal |
| 3.8.58 | Full dry-run of the 3.9.0 cut (branches, channels, forward-port) — includes the PR preview-artifact + build-once promotion rehearsal (#8084) |
| 3.8.59 | Final freeze · full-suite audit · GO/NO-GO |
## Phase 3 — v3.9.0 LTS
After 3.8.59 the next version is **3.9.0** (there is no 3.8.60). It creates the long-lived
branch model:
- **`stable/v3`** — the LTS line (3.9.x). Receives fixes, security patches, and provider
updates. `npm install omniroute` (aka `latest`) stays on v3 during the whole v4 cycle.
- **`develop`** — v4 development, published as `4.0.0-nightly.*`.
- **`main`** — v4 release candidates (`next`) and, eventually, GA.
- Fixes merged to `stable/v3` are automatically forward-ported to `develop` with full
contributor credit (`Co-authored-by`).
New features land in the v4 channel. The LTS line is stability-first.
## Phase 4 — v4.0: the modular platform
The monolith is intentionally disassembled on `develop`:
- **`@omniroute/core`** (npm name stays `omniroute`) — just the engine: `/v1/*`, routing,
combo/fallback, providers.
- **`@omniroute/sdk`** — one typed contract: hooks, extension points, two-phase lifecycle,
UI contributions. The five extension systems that exist today (plugins, CLI plugins,
skills, MCP tools, A2A skills) collapse into one declarative manifest.
- **Modules** (`@omniroute/mod-*`) — cloud agents, traffic inspection (MITM), evals,
webhooks, memory, guardrails, observability and more move out of the core, each with its
own version and lifecycle.
- **Providers as plugins** — adding a provider stops touching the core.
- **Marketplace** — one-click install with verified integrity (hash pinning, signing,
sandbox). Free in v1; a paid tier later with revenue share for creators.
- Ships as `4.0.0-nightly.*``4.0.0-rc.N` (soak in production) → **4.0.0 GA**, when
`latest` switches to v4 and v3 enters its announced LTS support window.
**The core is MIT and free, forever.**
## For contributors
| You are sending... | Target today | From 3.8.55 | After 3.9.0 |
| --- | --- | --- | --- |
| Bug fix / security | active `release/v3.8.x` | same | `stable/v3` |
| Provider update | active `release/v3.8.x` | same | `stable/v3` |
| Docs / i18n | active `release/v3.8.x` | same | `stable/v3` |
| New feature | active `release/v3.8.x` | held with `v4-feature` label | `develop` (v4) |
See `CONTRIBUTING.md` for the golden path per change type.

View File

@@ -209,14 +209,13 @@ on). Without a `max_concurrent` cap the behavior is unchanged.
### Combo cooldown-aware retry
For quota-share and `auto` combos, a request that would crystallize a 429 for a
SHORT transient cooldown waits it out and re-dispatches instead of returning
the 429 — this covers Gemini-class TPM/RPM windows (~60s retry-after) on a
multi-model `auto` combo, e.g. both targets of a 2-model combo hitting a
per-model rate limit. Bounded by `comboCooldownWait` (`enabled`, `maxWaitMs`
65s, `maxAttempts` 2, `budgetMs` 130s, hard ceiling 90s) in **Settings →
Resilience**. It never waits on `quota_exhausted` (locked until midnight) or
auth/not-found reasons.
For every combo strategy (when enabled), a request that would crystallize a 429
for a SHORT transient cooldown waits it out and re-dispatches instead of
returning the 429 — this covers Gemini-class TPM/RPM windows (~60s retry-after)
on multi-model combos, e.g. both targets of a 2-model combo hitting a per-model
rate limit. Bounded by `comboCooldownWait` (`enabled`, `maxWaitMs`, `maxAttempts`,
`budgetMs`) in **Settings → Resilience**. It never waits on `quota_exhausted`
(locked until midnight) or auth/not-found reasons.
---

View File

@@ -0,0 +1,138 @@
<svg viewBox="0 0 1200 780" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Comparison table: OmniRoute versus 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute is the only one with the full set: 290 providers, 90+ free providers built-in, 19 routing strategies, 12-engine token compression, a built-in MCP server with 104 tools, A2A protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, desktop/Termux/PWA, 43 UI locales and 100% MIT self-hosted. 9router has free providers, RTK compression and translation but no MCP, A2A, memory, guardrails, cloud agents or stealth. OpenRouter is a hosted SaaS with 400+ models, guardrails and a hosted MCP but is not self-hosted and lacks A2A, memory, cloud agents and stealth. CLIProxyAPI is a light OAuth proxy with two routing strategies. LiteLLM has 100+ providers, A2A and extensive guardrails but no memory, compression, free tier, stealth or cloud agents.">
<desc>Static-header comparison table where each capability row fades in top to bottom; the OmniRoute column is highlighted and shows a check or a leading value in every row, while competitors show a mix of checks, partials and crosses.</desc>
<defs>
<pattern id="gC" width="32" height="32" patternUnits="userSpaceOnUse"><path d="M 32 0 L 0 0 0 32" fill="none" stroke="#ffffff" stroke-opacity="0.05" stroke-width="1"/></pattern>
<radialGradient id="gGlow" cx="30%" cy="10%" r="60%"><stop offset="0%" stop-color="#7ee787" stop-opacity="0.10"/><stop offset="60%" stop-color="#00cec9" stop-opacity="0.04"/><stop offset="100%" stop-color="#6366f1" stop-opacity="0"/></radialGradient>
<g id="yes"><circle r="11" fill="#7ee787" fill-opacity="0.14"/><path d="M -5 0.3 L -1.6 3.7 L 5.4 -4" stroke="#7ee787" stroke-width="2.4" fill="none" stroke-linecap="round" stroke-linejoin="round"/></g>
<g id="no"><circle r="11" fill="#6e7681" fill-opacity="0.10"/><path d="M -3.8 -3.8 L 3.8 3.8 M 3.8 -3.8 L -3.8 3.8" stroke="#6e7681" stroke-width="2.1" stroke-linecap="round"/></g>
<g id="mid"><circle r="11" fill="#fdcb6e" fill-opacity="0.13"/><path d="M -4.6 0 L 4.6 0" stroke="#fdcb6e" stroke-width="2.4" stroke-linecap="round"/></g>
</defs>
<rect width="1200" height="780" fill="#0b0e14"/>
<rect width="1200" height="780" fill="url(#gC)"/>
<rect width="1200" height="780" fill="url(#gGlow)"/>
<text x="44" y="54" font-family="Consolas, 'Courier New', monospace" font-size="13" letter-spacing="2.5" fill="#7ee787">WHAT SETS OMNIROUTE APART</text>
<line x1="330" y1="49" x2="1156" y2="49" stroke="#232b38" stroke-width="1.5"/>
<text x="44" y="96" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="23" font-weight="600" fill="#c9d1d9">Every router does routing. <tspan fill="#7ee787" font-weight="800">OmniRoute does the whole platform.</tspan></text>
<rect x="352" y="126" width="176" height="606" rx="12" fill="#7ee787" fill-opacity="0.07" stroke="#7ee787" stroke-opacity="0.5" stroke-width="1.5"/>
<text x="440" y="150" text-anchor="middle" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="15.5" font-weight="800" fill="#7ee787">OmniRoute</text>
<text x="604" y="150" text-anchor="middle" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="14" font-weight="600" fill="#8b949e">9router</text>
<text x="760" y="150" text-anchor="middle" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="14" font-weight="600" fill="#8b949e">OpenRouter</text>
<text x="916" y="150" text-anchor="middle" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="14" font-weight="600" fill="#8b949e">CLIProxyAPI</text>
<text x="1080" y="150" text-anchor="middle" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="14" font-weight="600" fill="#8b949e">LiteLLM</text>
<line x1="44" y1="162" x2="1156" y2="162" stroke="#232b38" stroke-width="1.5"/>
<g font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif">
<g opacity="0"><animate attributeName="opacity" values="0;1" dur="0.4s" begin="0.15s" fill="freeze"/>
<text x="44" y="196" font-size="14.5" fill="#c9d1d9">Providers</text>
<text x="440" y="196" text-anchor="middle" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="15" font-weight="800" fill="#7ee787">290</text>
<text x="604" y="196" text-anchor="middle" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="13.5" font-weight="600" fill="#8b949e">40+</text>
<text x="760" y="196" text-anchor="middle" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="13.5" font-weight="600" fill="#8b949e">400+*</text>
<text x="916" y="196" text-anchor="middle" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="13.5" font-weight="600" fill="#8b949e">~5</text>
<text x="1080" y="196" text-anchor="middle" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="13.5" font-weight="600" fill="#8b949e">100+</text>
</g>
<rect x="36" y="212" width="1128" height="42" rx="6" fill="#ffffff" fill-opacity="0.02"/>
<g opacity="0"><animate attributeName="opacity" values="0;1" dur="0.4s" begin="0.24s" fill="freeze"/>
<text x="44" y="238" font-size="14.5" fill="#c9d1d9">Free providers built-in</text>
<text x="440" y="238" text-anchor="middle" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="15" font-weight="800" fill="#7ee787">90+</text>
<use href="#yes" x="604" y="233"/>
<use href="#mid" x="760" y="233"/>
<use href="#no" x="916" y="233"/>
<use href="#no" x="1080" y="233"/>
</g>
<g opacity="0"><animate attributeName="opacity" values="0;1" dur="0.4s" begin="0.33s" fill="freeze"/>
<text x="44" y="280" font-size="14.5" fill="#c9d1d9">Routing strategies</text>
<text x="440" y="280" text-anchor="middle" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="15" font-weight="800" fill="#7ee787">19</text>
<text x="604" y="280" text-anchor="middle" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="13.5" font-weight="600" fill="#8b949e">2</text>
<text x="760" y="280" text-anchor="middle" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="13.5" font-weight="600" fill="#8b949e">3</text>
<text x="916" y="280" text-anchor="middle" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="13.5" font-weight="600" fill="#8b949e">2</text>
<text x="1080" y="280" text-anchor="middle" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="13.5" font-weight="600" fill="#8b949e">6</text>
</g>
<rect x="36" y="296" width="1128" height="42" rx="6" fill="#ffffff" fill-opacity="0.02"/>
<g opacity="0"><animate attributeName="opacity" values="0;1" dur="0.4s" begin="0.42s" fill="freeze"/>
<text x="44" y="322" font-size="14.5" fill="#c9d1d9">Token compression</text>
<text x="440" y="322" text-anchor="middle" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="15" font-weight="800" fill="#7ee787">12 eng</text>
<use href="#yes" x="604" y="317"/>
<use href="#mid" x="760" y="317"/>
<use href="#no" x="916" y="317"/>
<use href="#no" x="1080" y="317"/>
</g>
<g opacity="0"><animate attributeName="opacity" values="0;1" dur="0.4s" begin="0.51s" fill="freeze"/>
<text x="44" y="364" font-size="14.5" fill="#c9d1d9">Built-in MCP server (own tools)</text>
<text x="440" y="364" text-anchor="middle" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="15" font-weight="800" fill="#7ee787">104</text>
<use href="#no" x="604" y="359"/>
<use href="#mid" x="760" y="359"/>
<use href="#no" x="916" y="359"/>
<use href="#mid" x="1080" y="359"/>
</g>
<rect x="36" y="380" width="1128" height="42" rx="6" fill="#ffffff" fill-opacity="0.02"/>
<g opacity="0"><animate attributeName="opacity" values="0;1" dur="0.4s" begin="0.60s" fill="freeze"/>
<text x="44" y="406" font-size="14.5" fill="#c9d1d9">A2A agent protocol</text>
<use href="#yes" x="440" y="401"/>
<use href="#no" x="604" y="401"/>
<use href="#no" x="760" y="401"/>
<use href="#no" x="916" y="401"/>
<use href="#yes" x="1080" y="401"/>
</g>
<g opacity="0"><animate attributeName="opacity" values="0;1" dur="0.4s" begin="0.69s" fill="freeze"/>
<text x="44" y="448" font-size="14.5" fill="#c9d1d9">Persistent memory</text>
<use href="#yes" x="440" y="443"/>
<use href="#no" x="604" y="443"/>
<use href="#no" x="760" y="443"/>
<use href="#no" x="916" y="443"/>
<use href="#no" x="1080" y="443"/>
</g>
<rect x="36" y="464" width="1128" height="42" rx="6" fill="#ffffff" fill-opacity="0.02"/>
<g opacity="0"><animate attributeName="opacity" values="0;1" dur="0.4s" begin="0.78s" fill="freeze"/>
<text x="44" y="490" font-size="14.5" fill="#c9d1d9">Guardrails (PII / injection)</text>
<use href="#yes" x="440" y="485"/>
<use href="#no" x="604" y="485"/>
<use href="#yes" x="760" y="485"/>
<use href="#no" x="916" y="485"/>
<use href="#yes" x="1080" y="485"/>
</g>
<g opacity="0"><animate attributeName="opacity" values="0;1" dur="0.4s" begin="0.87s" fill="freeze"/>
<text x="44" y="532" font-size="14.5" fill="#c9d1d9">Cloud agents (Codex/Devin/Jules)</text>
<use href="#yes" x="440" y="527"/>
<use href="#no" x="604" y="527"/>
<use href="#no" x="760" y="527"/>
<use href="#no" x="916" y="527"/>
<use href="#no" x="1080" y="527"/>
</g>
<rect x="36" y="548" width="1128" height="42" rx="6" fill="#ffffff" fill-opacity="0.02"/>
<g opacity="0"><animate attributeName="opacity" values="0;1" dur="0.4s" begin="0.96s" fill="freeze"/>
<text x="44" y="574" font-size="14.5" fill="#c9d1d9">TLS fingerprint stealth (JA3/JA4)</text>
<use href="#yes" x="440" y="569"/>
<use href="#no" x="604" y="569"/>
<use href="#no" x="760" y="569"/>
<use href="#no" x="916" y="569"/>
<use href="#no" x="1080" y="569"/>
</g>
<g opacity="0"><animate attributeName="opacity" values="0;1" dur="0.4s" begin="1.05s" fill="freeze"/>
<text x="44" y="616" font-size="14.5" fill="#c9d1d9">Desktop · Termux · PWA</text>
<use href="#yes" x="440" y="611"/>
<use href="#mid" x="604" y="611"/>
<use href="#mid" x="760" y="611"/>
<use href="#mid" x="916" y="611"/>
<use href="#mid" x="1080" y="611"/>
</g>
<rect x="36" y="632" width="1128" height="42" rx="6" fill="#ffffff" fill-opacity="0.02"/>
<g opacity="0"><animate attributeName="opacity" values="0;1" dur="0.4s" begin="1.14s" fill="freeze"/>
<text x="44" y="658" font-size="14.5" fill="#c9d1d9">i18n UI locales</text>
<text x="440" y="658" text-anchor="middle" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="15" font-weight="800" fill="#7ee787">43</text>
<text x="604" y="658" text-anchor="middle" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="13.5" font-weight="600" fill="#8b949e">6</text>
<use href="#no" x="760" y="653"/>
<use href="#no" x="916" y="653"/>
<use href="#no" x="1080" y="653"/>
</g>
<g opacity="0"><animate attributeName="opacity" values="0;1" dur="0.4s" begin="1.23s" fill="freeze"/>
<text x="44" y="700" font-size="14.5" fill="#c9d1d9">Self-hosted · 100% MIT</text>
<text x="440" y="700" text-anchor="middle" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="15" font-weight="800" fill="#7ee787">MIT</text>
<text x="604" y="700" text-anchor="middle" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="13.5" font-weight="600" fill="#8b949e">MIT</text>
<use href="#no" x="760" y="695"/>
<text x="916" y="700" text-anchor="middle" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="13.5" font-weight="600" fill="#8b949e">MIT</text>
<text x="1080" y="700" text-anchor="middle" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="13.5" font-weight="600" fill="#fdcb6e">open-core</text>
</g>
</g>
<line x1="44" y1="728" x2="1156" y2="728" stroke="#232b38" stroke-width="1.5"/>
<text x="44" y="750" font-family="Consolas, 'Courier New', monospace" font-size="12" fill="#71717a">full &#8226; partial &#8226; none&#160;&#160;·&#160;&#160;*OpenRouter counts models &amp; is a hosted SaaS, not self-hosted.</text>
<text x="1156" y="750" text-anchor="end" font-family="Consolas, 'Courier New', monospace" font-size="12" letter-spacing="1.5" fill="#71717a">verified from each project's docs</text>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,117 @@
<svg viewBox="0 0 1200 430" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Works the second you install it — zero config. Three steps: 1. Install — npm install -g omniroute, one command, server on localhost port 20128. 2. Point your tool at http://localhost:20128/v1 — any OpenAI-compatible tool. 3. It answers — call model auto and get an instant reply with no API key, no signup, no configuration. Keyless free providers OpenCode Free and Felo are pre-wired into the auto combo, so a fresh install responds out of the box.">
<desc>Animated flow card: three step tiles (Install, Point your tool, It answers) fade in left to right, a dot travels along the connectors between them in a loop, and the final check pulses.</desc>
<defs>
<pattern id="gridW" width="32" height="32" patternUnits="userSpaceOnUse">
<path d="M 32 0 L 0 0 0 32" fill="none" stroke="#ffffff" stroke-opacity="0.06" stroke-width="1"/>
</pattern>
<radialGradient id="bgGlowW" cx="72%" cy="12%" r="60%">
<stop offset="0%" stop-color="#7ee787" stop-opacity="0.12"/>
<stop offset="55%" stop-color="#00cec9" stop-opacity="0.06"/>
<stop offset="100%" stop-color="#6366f1" stop-opacity="0"/>
</radialGradient>
</defs>
<rect width="1200" height="430" fill="#0b0e14"/>
<rect width="1200" height="430" fill="url(#gridW)"/>
<rect width="1200" height="430" fill="url(#bgGlowW)"/>
<!-- header -->
<text x="40" y="56" font-family="Consolas, 'Courier New', monospace" font-size="13" letter-spacing="2.5" fill="#7ee787">WORKS THE SECOND YOU INSTALL IT</text>
<line x1="392" y1="51" x2="1160" y2="51" stroke="#232b38" stroke-width="1.5"/>
<text x="40" y="98" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="23" font-weight="600" fill="#c9d1d9">Fresh install → it already answers. <tspan fill="#7ee787" font-weight="800">Zero config.</tspan></text>
<!-- connectors (drawn under the tiles) -->
<g stroke="#2b3543" stroke-width="2" stroke-dasharray="5 5" fill="none">
<path id="cW1" d="M 372 218 L 432 218"/>
<path id="cW2" d="M 767 218 L 827 218"/>
</g>
<!-- traveling dots along the connectors -->
<circle r="4" fill="#a78bfa">
<animateMotion dur="2.6s" begin="1.4s" repeatCount="indefinite" keyPoints="0;1" keyTimes="0;1" calcMode="linear">
<mpath href="#cW1"/>
</animateMotion>
<animate attributeName="opacity" values="0;1;1;0" keyTimes="0;0.15;0.85;1" dur="2.6s" begin="1.4s" repeatCount="indefinite"/>
</circle>
<circle r="4" fill="#7ee787">
<animateMotion dur="2.6s" begin="1.9s" repeatCount="indefinite" keyPoints="0;1" keyTimes="0;1" calcMode="linear">
<mpath href="#cW2"/>
</animateMotion>
<animate attributeName="opacity" values="0;1;1;0" keyTimes="0;0.15;0.85;1" dur="2.6s" begin="1.9s" repeatCount="indefinite"/>
</circle>
<g font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif">
<!-- step 1: install (blue) -->
<g opacity="0">
<animate attributeName="opacity" values="0;1" dur="0.5s" begin="0.1s" fill="freeze"/>
<rect x="40" y="132" width="332" height="172" rx="14" fill="#161b22" stroke="#ffffff" stroke-opacity="0.08" stroke-width="1"/>
<rect x="40" y="148" width="4" height="140" rx="2" fill="#0984e3"/>
<circle cx="80" cy="172" r="15" fill="#74b9ff" fill-opacity="0.12"/>
<text x="80" y="178" text-anchor="middle" font-family="Consolas, 'Courier New', monospace" font-size="15" font-weight="800" fill="#74b9ff">1</text>
<g transform="translate(108,161)" stroke="#74b9ff" stroke-width="2.2" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path d="M 11 2 L 11 15 M 6 10 L 11 15 L 16 10"/>
<path d="M 3 19 L 19 19"/>
</g>
<text x="140" y="179" font-size="18" font-weight="800" fill="#74b9ff">Install</text>
<text x="66" y="222" font-family="Consolas, 'Courier New', monospace" font-size="14" fill="#e6edf3">$ npm i -g omniroute</text>
<text x="66" y="258" font-size="13.5" fill="#a1a1aa">One command. Server boots on</text>
<text x="66" y="279" font-family="Consolas, 'Courier New', monospace" font-size="13" fill="#8b949e">localhost:20128</text>
</g>
<!-- step 2: point your tool (violet) -->
<g opacity="0">
<animate attributeName="opacity" values="0;1" dur="0.5s" begin="0.5s" fill="freeze"/>
<rect x="435" y="132" width="332" height="172" rx="14" fill="#161b22" stroke="#ffffff" stroke-opacity="0.08" stroke-width="1"/>
<rect x="435" y="148" width="4" height="140" rx="2" fill="#8b5cf6"/>
<circle cx="475" cy="172" r="15" fill="#a78bfa" fill-opacity="0.12"/>
<text x="475" y="178" text-anchor="middle" font-family="Consolas, 'Courier New', monospace" font-size="15" font-weight="800" fill="#a78bfa">2</text>
<g transform="translate(503,161)" stroke="#a78bfa" stroke-width="2.2" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path d="M 8 6 L 5 9 A 4 4 0 0 0 5 15 L 8 18 M 14 6 L 17 9 A 4 4 0 0 1 17 15 L 14 18 M 8.5 12 L 13.5 12"/>
</g>
<text x="535" y="179" font-size="18" font-weight="800" fill="#a78bfa">Point your tool</text>
<text x="461" y="222" font-family="Consolas, 'Courier New', monospace" font-size="13" fill="#e6edf3">http://localhost:20128/v1</text>
<text x="461" y="258" font-size="13.5" fill="#a1a1aa">Any OpenAI-compatible tool —</text>
<text x="461" y="279" font-size="13.5" fill="#a1a1aa">Claude Code, Cursor, Cline…</text>
</g>
<!-- step 3: it answers (green) -->
<g opacity="0">
<animate attributeName="opacity" values="0;1" dur="0.5s" begin="0.9s" fill="freeze"/>
<rect x="830" y="132" width="330" height="172" rx="14" fill="#161b22" stroke="#ffffff" stroke-opacity="0.08" stroke-width="1"/>
<rect x="830" y="132" width="330" height="172" rx="14" fill="none" stroke="#7ee787" stroke-width="1.5" opacity="0">
<animate attributeName="opacity" values="0;0.9;0" keyTimes="0;0.5;1" dur="2.6s" begin="3.2s" repeatCount="indefinite"/>
</rect>
<rect x="830" y="148" width="4" height="140" rx="2" fill="#22c55e"/>
<circle cx="870" cy="172" r="15" fill="#7ee787" fill-opacity="0.12"/>
<text x="870" y="178" text-anchor="middle" font-family="Consolas, 'Courier New', monospace" font-size="15" font-weight="800" fill="#7ee787">3</text>
<g transform="translate(898,161)" stroke="#7ee787" stroke-width="2.4" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path d="M 3 11 L 9 17 L 19 5"/>
</g>
<text x="930" y="179" font-size="18" font-weight="800" fill="#7ee787">It answers</text>
<text x="856" y="222" font-family="Consolas, 'Courier New', monospace" font-size="14" fill="#e6edf3">model: <tspan fill="#7ee787" font-weight="700">"auto"</tspan> → reply</text>
<text x="856" y="258" font-size="13.5" fill="#a1a1aa">Instant. OmniRoute builds a virtual</text>
<text x="856" y="279" font-size="13.5" fill="#a1a1aa">combo from keyless free providers.</text>
</g>
</g>
<!-- "no ___" chips -->
<g font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="13.5" font-weight="700" opacity="0">
<animate attributeName="opacity" values="0;1" dur="0.5s" begin="1.3s" fill="freeze"/>
<g transform="translate(40,332)">
<rect x="0" y="0" width="168" height="34" rx="17" fill="#161b22" stroke="#7ee787" stroke-opacity="0.35" stroke-width="1"/>
<text x="24" y="22" fill="#c9d1d9">✕ no API key</text>
</g>
<g transform="translate(224,332)">
<rect x="0" y="0" width="168" height="34" rx="17" fill="#161b22" stroke="#7ee787" stroke-opacity="0.35" stroke-width="1"/>
<text x="24" y="22" fill="#c9d1d9">✕ no signup</text>
</g>
<g transform="translate(408,332)">
<rect x="0" y="0" width="188" height="34" rx="17" fill="#161b22" stroke="#7ee787" stroke-opacity="0.35" stroke-width="1"/>
<text x="24" y="22" fill="#c9d1d9">✕ no configuration</text>
</g>
</g>
<!-- footer -->
<text x="40" y="406" font-family="Consolas, 'Courier New', monospace" font-size="12" fill="#71717a">OpenCode Free &#38; Felo are pre-wired into <tspan fill="#7ee787">auto</tspan> — a fresh install responds out of the box.</text>
<text x="1160" y="406" text-anchor="end" font-family="Consolas, 'Courier New', monospace" font-size="12" letter-spacing="1.5" fill="#71717a">$0 · MIT</text>
</svg>

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@@ -1,7 +1,7 @@
---
title: "Claude Code CLI — Configuration with OmniRoute"
version: 3.8.40
lastUpdated: 2026-06-28
lastUpdated: 2026-07-24
---
# Claude Code CLI — Configuration with OmniRoute
@@ -140,10 +140,20 @@ extra flags needed. Override per-invocation with `--remote` / `--api-key`.
handles this for you.
**`/model` picker is empty / missing gateway models** — needs Claude Code
v2.1.129+ and `CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1`. Only `claude*` /
v2.1.219+ and `CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1`. Only `claude*` /
`anthropic*` model IDs appear in the picker; force any other model with
`ANTHROPIC_MODEL=<id>` (this is what profiles do).
**`400 Ambiguous model 'claude-…'`** — Claude Code always sends **unprefixed**
model IDs (e.g. `claude-opus-4-8`), so when both the Claude Code (`cc/…`) and
Claude (`claude/…`) providers are connected the bare id matches two routes and
OmniRoute refuses to guess. Fix it either way: pin a prefixed id with
`ANTHROPIC_MODEL=cc/claude-opus-4-8`, or enable **Prefer Claude Code for
unprefixed Claude models** — the toggle on the Claude provider page, or
`OMNIROUTE_PREFER_CLAUDE_CODE_FOR_UNPREFIXED_CLAUDE_MODELS=true` (default off;
see [Environment](../reference/ENVIRONMENT.md)) — which routes bare `claude-*`
IDs to Claude Code instead. Explicit provider prefixes always win.
**Auth errors** — the profile holds no token. Use `omniroute launch --profile`
(injects it) or export `ANTHROPIC_AUTH_TOKEN`.

View File

@@ -572,7 +572,7 @@ For the full environment variable reference, see the [README](../README.md).
**GitHub Copilot (`gh/`)** — OAuth: `gh/gpt-5.5`, `gh/gpt-5.4`, `gh/gpt-5.4-mini`, `gh/gpt-5-mini`, `gh/gpt-5.3-codex`, `gh/claude-opus-4.7`, `gh/claude-opus-4.6`, `gh/claude-opus-4-5-20251101`, `gh/claude-sonnet-4.6`, `gh/claude-sonnet-4.5`, `gh/claude-haiku-4.5`, `gh/gemini-3.1-pro-preview`, `gh/gemini-3-flash-preview`, `gh/oswe-vscode-prime`
**Kiro (`kr/`)** — FREE OAuth: `kr/auto-kiro`, `kr/claude-opus-4.7`, `kr/claude-opus-4.6`, `kr/claude-sonnet-4.6`, `kr/claude-sonnet-4.5`, `kr/claude-haiku-4.5`, `kr/deepseek-3.2`, `kr/minimax-m2.5`, `kr/minimax-m2.1`, `kr/glm-5`, `kr/qwen3-coder-next`
**Kiro (`kr/`)** — FREE OAuth: use the live catalog shown under **Dashboard → Providers → Kiro → Available Models**. Availability depends on the account and plan.
**Qoder (`if/`)** — FREE OAuth: `if/qwen3.8-max-preview`, `if/qwen3.7-max`, `if/qwen3.7-plus`, `if/kimi-k3`, `if/kimi-k2.7-code`, `if/glm-5.2`, `if/deepseek-v4-pro`, `if/deepseek-v4-flash`, `if/minimax-m3`

View File

@@ -336,7 +336,7 @@ process.env[`${PROVIDER_ID}_USER_AGENT`]
| Variable | Default Value | When to Update |
| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- |
| `CLAUDE_USER_AGENT` | `claude-cli/2.1.145 (external, cli)` | When Anthropic releases a new CLI version |
| `CLAUDE_USER_AGENT` | `claude-cli/2.1.219 (external, cli)` | When Anthropic releases a new CLI version |
| `CODEX_USER_AGENT` | `codex-cli/0.132.0 (Windows 10.0.26200; x64)` | When OpenAI updates the Codex CLI |
| `CODEX_CLIENT_VERSION` | `0.131.0` | Override Codex client version independently of full UA string |
| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates |

View File

@@ -336,7 +336,7 @@ process.env[`${PROVIDER_ID}_USER_AGENT`]
| Variable | Default Value | When to Update |
| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- |
| `CLAUDE_USER_AGENT` | `claude-cli/2.1.145 (external, cli)` | When Anthropic releases a new CLI version |
| `CLAUDE_USER_AGENT` | `claude-cli/2.1.219 (external, cli)` | When Anthropic releases a new CLI version |
| `CODEX_USER_AGENT` | `codex-cli/0.132.0 (Windows 10.0.26200; x64)` | When OpenAI updates the Codex CLI |
| `CODEX_CLIENT_VERSION` | `0.131.0` | Override Codex client version independently of full UA string |
| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates |

View File

@@ -336,7 +336,7 @@ process.env[`${PROVIDER_ID}_USER_AGENT`]
| Variable | Default Value | When to Update |
| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- |
| `CLAUDE_USER_AGENT` | `claude-cli/2.1.145 (external, cli)` | When Anthropic releases a new CLI version |
| `CLAUDE_USER_AGENT` | `claude-cli/2.1.219 (external, cli)` | When Anthropic releases a new CLI version |
| `CODEX_USER_AGENT` | `codex-cli/0.132.0 (Windows 10.0.26200; x64)` | When OpenAI updates the Codex CLI |
| `CODEX_CLIENT_VERSION` | `0.131.0` | Override Codex client version independently of full UA string |
| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates |

View File

@@ -336,7 +336,7 @@ process.env[`${PROVIDER_ID}_USER_AGENT`]
| Variable | Default Value | When to Update |
| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- |
| `CLAUDE_USER_AGENT` | `claude-cli/2.1.145 (external, cli)` | When Anthropic releases a new CLI version |
| `CLAUDE_USER_AGENT` | `claude-cli/2.1.219 (external, cli)` | When Anthropic releases a new CLI version |
| `CODEX_USER_AGENT` | `codex-cli/0.132.0 (Windows 10.0.26200; x64)` | When OpenAI updates the Codex CLI |
| `CODEX_CLIENT_VERSION` | `0.131.0` | Override Codex client version independently of full UA string |
| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates |

View File

@@ -336,7 +336,7 @@ process.env[`${PROVIDER_ID}_USER_AGENT`]
| Variable | Default Value | When to Update |
| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- |
| `CLAUDE_USER_AGENT` | `claude-cli/2.1.145 (external, cli)` | When Anthropic releases a new CLI version |
| `CLAUDE_USER_AGENT` | `claude-cli/2.1.219 (external, cli)` | When Anthropic releases a new CLI version |
| `CODEX_USER_AGENT` | `codex-cli/0.132.0 (Windows 10.0.26200; x64)` | When OpenAI updates the Codex CLI |
| `CODEX_CLIENT_VERSION` | `0.131.0` | Override Codex client version independently of full UA string |
| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates |

View File

@@ -336,7 +336,7 @@ process.env[`${PROVIDER_ID}_USER_AGENT`]
| Variable | Default Value | When to Update |
| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- |
| `CLAUDE_USER_AGENT` | `claude-cli/2.1.145 (external, cli)` | When Anthropic releases a new CLI version |
| `CLAUDE_USER_AGENT` | `claude-cli/2.1.219 (external, cli)` | When Anthropic releases a new CLI version |
| `CODEX_USER_AGENT` | `codex-cli/0.132.0 (Windows 10.0.26200; x64)` | When OpenAI updates the Codex CLI |
| `CODEX_CLIENT_VERSION` | `0.131.0` | Override Codex client version independently of full UA string |
| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates |

View File

@@ -336,7 +336,7 @@ process.env[`${PROVIDER_ID}_USER_AGENT`]
| Variable | Default Value | When to Update |
| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- |
| `CLAUDE_USER_AGENT` | `claude-cli/2.1.145 (external, cli)` | When Anthropic releases a new CLI version |
| `CLAUDE_USER_AGENT` | `claude-cli/2.1.219 (external, cli)` | When Anthropic releases a new CLI version |
| `CODEX_USER_AGENT` | `codex-cli/0.132.0 (Windows 10.0.26200; x64)` | When OpenAI updates the Codex CLI |
| `CODEX_CLIENT_VERSION` | `0.131.0` | Override Codex client version independently of full UA string |
| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates |

View File

@@ -336,7 +336,7 @@ process.env[`${PROVIDER_ID}_USER_AGENT`]
| Variable | Default Value | When to Update |
| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- |
| `CLAUDE_USER_AGENT` | `claude-cli/2.1.145 (external, cli)` | When Anthropic releases a new CLI version |
| `CLAUDE_USER_AGENT` | `claude-cli/2.1.219 (external, cli)` | When Anthropic releases a new CLI version |
| `CODEX_USER_AGENT` | `codex-cli/0.132.0 (Windows 10.0.26200; x64)` | When OpenAI updates the Codex CLI |
| `CODEX_CLIENT_VERSION` | `0.131.0` | Override Codex client version independently of full UA string |
| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates |

View File

@@ -336,7 +336,7 @@ process.env[`${PROVIDER_ID}_USER_AGENT`]
| Variable | Default Value | When to Update |
| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- |
| `CLAUDE_USER_AGENT` | `claude-cli/2.1.145 (external, cli)` | When Anthropic releases a new CLI version |
| `CLAUDE_USER_AGENT` | `claude-cli/2.1.219 (external, cli)` | When Anthropic releases a new CLI version |
| `CODEX_USER_AGENT` | `codex-cli/0.132.0 (Windows 10.0.26200; x64)` | When OpenAI updates the Codex CLI |
| `CODEX_CLIENT_VERSION` | `0.131.0` | Override Codex client version independently of full UA string |
| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates |

View File

@@ -336,7 +336,7 @@ process.env[`${PROVIDER_ID}_USER_AGENT`]
| Variable | Default Value | When to Update |
| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- |
| `CLAUDE_USER_AGENT` | `claude-cli/2.1.145 (external, cli)` | When Anthropic releases a new CLI version |
| `CLAUDE_USER_AGENT` | `claude-cli/2.1.219 (external, cli)` | When Anthropic releases a new CLI version |
| `CODEX_USER_AGENT` | `codex-cli/0.132.0 (Windows 10.0.26200; x64)` | When OpenAI updates the Codex CLI |
| `CODEX_CLIENT_VERSION` | `0.131.0` | Override Codex client version independently of full UA string |
| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates |

View File

@@ -336,7 +336,7 @@ process.env[`${PROVIDER_ID}_USER_AGENT`]
| Variable | Default Value | When to Update |
| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- |
| `CLAUDE_USER_AGENT` | `claude-cli/2.1.145 (external, cli)` | When Anthropic releases a new CLI version |
| `CLAUDE_USER_AGENT` | `claude-cli/2.1.219 (external, cli)` | When Anthropic releases a new CLI version |
| `CODEX_USER_AGENT` | `codex-cli/0.132.0 (Windows 10.0.26200; x64)` | When OpenAI updates the Codex CLI |
| `CODEX_CLIENT_VERSION` | `0.131.0` | Override Codex client version independently of full UA string |
| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates |

View File

@@ -336,7 +336,7 @@ process.env[`${PROVIDER_ID}_USER_AGENT`]
| Variable | Default Value | When to Update |
| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- |
| `CLAUDE_USER_AGENT` | `claude-cli/2.1.145 (external, cli)` | When Anthropic releases a new CLI version |
| `CLAUDE_USER_AGENT` | `claude-cli/2.1.219 (external, cli)` | When Anthropic releases a new CLI version |
| `CODEX_USER_AGENT` | `codex-cli/0.132.0 (Windows 10.0.26200; x64)` | When OpenAI updates the Codex CLI |
| `CODEX_CLIENT_VERSION` | `0.131.0` | Override Codex client version independently of full UA string |
| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates |

View File

@@ -336,7 +336,7 @@ process.env[`${PROVIDER_ID}_USER_AGENT`]
| Variable | Default Value | When to Update |
| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- |
| `CLAUDE_USER_AGENT` | `claude-cli/2.1.145 (external, cli)` | When Anthropic releases a new CLI version |
| `CLAUDE_USER_AGENT` | `claude-cli/2.1.219 (external, cli)` | When Anthropic releases a new CLI version |
| `CODEX_USER_AGENT` | `codex-cli/0.132.0 (Windows 10.0.26200; x64)` | When OpenAI updates the Codex CLI |
| `CODEX_CLIENT_VERSION` | `0.131.0` | Override Codex client version independently of full UA string |
| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates |

View File

@@ -336,7 +336,7 @@ process.env[`${PROVIDER_ID}_USER_AGENT`]
| Variable | Default Value | When to Update |
| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- |
| `CLAUDE_USER_AGENT` | `claude-cli/2.1.145 (external, cli)` | When Anthropic releases a new CLI version |
| `CLAUDE_USER_AGENT` | `claude-cli/2.1.219 (external, cli)` | When Anthropic releases a new CLI version |
| `CODEX_USER_AGENT` | `codex-cli/0.132.0 (Windows 10.0.26200; x64)` | When OpenAI updates the Codex CLI |
| `CODEX_CLIENT_VERSION` | `0.131.0` | Override Codex client version independently of full UA string |
| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.45.1` | When GitHub Copilot Chat updates |

Some files were not shown because too many files have changed in this diff Show More