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.
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>
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>
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>
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>
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>
* 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>
`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.
`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.
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.
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.
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.
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
* 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.
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
* 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>
* 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.
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.
* 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.
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>
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>
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>
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(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>
* 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>
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
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
* 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
* 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
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
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
#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
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
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
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
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
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
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(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>
- 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.
- Added Sweden flag (se.svg)
- Added Slovakia flag (sk.svg)
- Added Thailand flag (th.svg)
- Added Turkey flag (tr.svg)
- Added Taiwan flag (tw.svg)
- Added Tanzania flag (tz.svg)
- Added Ukraine flag (ua.svg)
- Added United States flag (us.svg)
- Added Vietnam flag (vn.svg)
Node/undici fetch is rejected by Notion's edge with HTTP 200
temporarily-unavailable and empty assistant text (messages appear in the
thread, UI shows 502 No response from Notion AI). The same cookie and body
succeed via curl/Schannel and a browser Chrome JA3 handshake.
Route inference through tls-client-node (chrome_146), matching Claude/
Perplexity web providers. Also detect nested patch-start error objects so
operators see temporarily-unavailable instead of a misleading empty-body
502, and treat that subtype as retryable.
Verified live: notion-web/fable-5, hyperagent/fable, and
promptql/vertex-claude-fable-5 all return PONG through the packaged
backend; unit tests 83/83.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(bifrost): send v-prefixed transport version, not bare semver
bifrost.ts resolveSpawnArgs() set BIFROST_TRANSPORT_VERSION straight from
getInstalledVersionSync(), which reads the raw "version" field out of
node_modules/@maximhq/bifrost/package.json - always bare semver per npm
convention (e.g. "1.6.3").
@maximhq/bifrost's own bin.js validates that env var against
/^v\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/ or the literal "latest" and rejects
anything else with "Invalid transport version format", exiting immediately.
Every embedded Bifrost instance failed to start as a result.
Adds formatTransportVersion() to normalize at the call site that owns the
env var, so bifrost.ts and the upstream @maximhq/bifrost package both stay
exactly as designed - no changes needed to bifrost itself.
Strengthens the existing resolveSpawnArgs test to assert the actual v-prefix
format (it previously only checked for a non-empty string, the same gap
#6877 called out for cliproxy's pre-existing test), and adds a dedicated
regression test file covering the pure formatTransportVersion() helper plus
a real-filesystem resolveSpawnArgs() integration check.
Found and fixed while self-hosting OmniRoute and diagnosing why bifrost
kept crash-looping on startup.
* fix(bifrost): resolve install dir lazily so version read honors runtime DATA_DIR
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: seanford <seanford@users.noreply.github.com>
* feat(chaos+ponytail): parallel chaos-mode dispatch + ponytail output style (rebased on v3.8.49)
- Chaos mode: new auto/chaos variant fans the prompt out to the top-N
stable models in parallel and returns a single merged SSE stream.
- Progressive streaming: each panel model's answer is enqueued as it
lands (omni-chaos-part event), instead of awaiting the whole panel.
- withTimeout now aborts the underlying request (modelAbortSignal) on
timeout so the connection is released, not leaked.
- concatSseText parses both OpenAI and Anthropic SSE wire formats.
- autoPrefix/modePacks add the chaos-mode weight pack; virtualFactory
materializes auto/chaos with fusion strategy + chaos config flag.
- Ponytail (lazy-senior-dev mode) integrated into the existing
OUTPUT_STYLE_CATALOG registry (id 'ponytail') so it rides the production
output-style injector, instead of a bespoke duplicate module. Dev-only
scripts and the duplicate ponytail/ module are removed.
- Tests: chaosEngine/chaosVirtualCombo cover panel dispatch, progressive
broadcast, timeout abort, and Anthropic parsing; autoCombo pack count
updated to 6.
Rebased onto release/v3.8.49 (no provider-registry or validation changes —
those are split out per review).
* optimize(chaos+ponytail): i18n ponytail, dedupl chaos dispatch, provider diversity
- Ponytail: add vi/ja/pt-BR/id i18n with lite/full/ultra levels
- chaosEngine: extract dispatchOnePanelModel (shared), add onResult for
progressive SSE streaming, fix withTimeout anti-pattern
- virtualFactory: deduplicate chaos panel by provider, add tuning overrides
- dispatchChaosFromCombo: accept ChaosTuning, enforce minPanel
- Add/port 8 node-runner tests for ponytail i18n + catalog integrity
- Add muse-spark-web.ts to KNOWN_MISSING_ERROR_HELPER (pre-existing)
* fix(8264): use HandleSingleModel type in chaosEngine dispatch (base-drift)
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* 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)
* fix(usage): correct token/request counting for 30D/90D/YTD/ALL ranges
Two bugs caused incorrect usage statistics for date ranges beyond the
raw data retention window:
1. Cutoff mismatch: the analytics route computed rawCutoffDate from
aggregation.rawDataRetentionDays (migration 046 seeds =7) while
cleanupUsageHistory rolls up and deletes at retention.usageHistory
(=30). The window [day-30, day-7) existed in usage_history but was
excluded from BOTH UNION legs — raw leg floored at day-7, aggregated
leg ended at day-7 — producing undercounted token sums for 30D,
90D, YTD, and ALL ranges.
Fix: use dbSettings.retention.usageHistory for the raw cutoff in
both route.ts and getRawDataCutoffDate() (aggregateHistory.ts),
matching the actual cleanup boundary.
2. Request undercount: COUNT(*) on the unified source counted each
daily_usage_summary row as 1, not total_requests. A day with 50
rolled-up requests counted as 1.
Fix: add a 'requests' column to both UNION legs (raw: 1, aggregated:
total_requests), change COUNT(*) to SUM(requests) in 6 query
functions, and change successfulRequests from
SUM(CASE WHEN success=1 THEN 1 ELSE 0 END) to
SUM(CASE WHEN success=1 THEN requests ELSE 0 END). Also set
agg leg latency_ms to NULL so AVG(latency_ms) is not skewed.
Tests: 33/33 source-level tests pass (db-usageanalytics-split.test.ts),
verifying 'requests' column presence and SUM(requests) usage in all
affected queries. DB-level integration test added to
usage-analytics.test.ts (requires node + better-sqlite3).
* fix(ci): green CI reds on #7300 — file-size ratchet, stale test fixture, shallow-checkout selfref test
- src/app/api/usage/analytics/route.ts: trim the new comment to keep the file
at the frozen file-size baseline (942 lines) after the retention.usageHistory
cutoff fix — no logic change.
- tests/unit/usage-analytics-route.test.ts: the pre-existing "does not
double-count raw and aggregated rows" test hardcoded a 30-day cutoff that
matched the OLD (buggy) aggregation.rawDataRetentionDays default. Now that
the raw/aggregated boundary correctly uses retention.usageHistory (365 days
by default, matching cleanupUsageHistory's actual rollup/delete boundary),
the fixture's synthetic "old" row was within the raw window and got
excluded from the aggregated leg. Read the real retention setting instead
of hardcoding 30 so the fixture reflects the corrected boundary. Same
assertions (still expects no double-counting, totalRequests=2,
totalTokens=185) — only the fixture dates change.
- tests/unit/check-test-masking-selfref-6634.test.ts: tolerate the shallow/
single-ref checkout used by GitHub-hosted Unit Tests runners (no local
origin/main ref) by fetching it on demand and skipping (never failing) when
unreachable offline. Matches the fix already applied on another branch
(2e42b8efc/#7174) for the same root cause, not yet on main.
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
* test(ci): make the #6634 selfref test checkout-independent (read the real file, no git ref)
The previous on-demand `git fetch origin main` + t.skip() fallback cleared the
shallow-checkout failure but tripped the PR Test Policy's test-masking gate
(a new .skip counts as a silenced assert — correctly so).
Drop the git dependency entirely instead: read the REAL current source of
tests/unit/check-test-masking.test.ts from disk (so the actual #6404 fixture
literals stay under test) and model the pre-#6404 state with an empty base,
which maximizes headTaut - baseTaut — the strictest input for the exclusion
this test asserts. No skip, no weakened assertion, same deepEqual guarantee.
Verified non-vacuous: neutralizing SELF_TEST_FIXTURE_RE in
scripts/check/check-test-masking.mjs makes this test fail; restoring it makes
it pass.
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
* 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.
---------
Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
sanitizeStreamingChunk() only stripped zero-width characters (U+200B,
U+200C, U+200D, U+FEFF) from OpenAI-format choices[].delta.content.
Anthropic-native content_block_delta events with text_delta or
thinking_delta payloads bypassed that path entirely, leaking U+200D
to clients on the Messages API streaming route.
Add a content_block_delta branch that strips zero-width characters
from delta.text and delta.thinking before returning the event,
matching the existing OpenAI path behavior from #5857.
Co-authored-by: Austin Liu <austinliu@Austins-MacBook-Air-3.local>
Background token health check sweep was probing connections with
terminal statuses (credits_exhausted / banned / expired) on every
cycle, wasting CPU and network. These connections can never self-heal
via token refresh — they need manual re-auth or credit top-up.
Add a terminal status guard in checkConnection() that mirrors the
existing isTerminalConnectionStatus() in auth.ts and
TERMINAL_CONNECTION_STATUSES in connectionRecovery.ts.
Verified by reporter: after manually disabling 11 credits_exhausted
connections, CPU dropped from ~53% to ~2.7%. This fix automates that
skip so the sweep never touches terminal connections in the first place.
Co-authored-by: Austin Liu <austinliu@Austins-MacBook-Air-3.local>
spawn('claude') without shell:true cannot resolve the .cmd shim
that npm installs on Windows, causing ENOENT and a misleading
'not found in PATH' error. Use claude.cmd + shell:true + windowsHide
on win32, matching the pattern already used in launch-codex.mjs.
Co-authored-by: Austin Liu <austinliu@Austins-MacBook-Air-3.local>
#8232 correctly targeted OAuth auto-refresh for legacy stored Gemini CLI connections, but exceeded that compatibility goal by recreating a complete routable and UI-visible provider with an Antigravity model catalog.
Preserve legacy refresh by mapping gemini-cli rows to the existing Gemini OAuth credentials. Remove the public provider registry, OAuth preset, model routing snapshot, and canonical-provider tests while keeping Gemini API and Antigravity unchanged.
#2101 still hard-stopped model-not-supported failures when upstream
wrapped them as invalid_request_error / Bad Request. Keep models in the
combo and try the next target (#8251, residual of #5249).
- export MODEL_ACCESS_DENIED_PATTERNS + broaden does-not-support shapes
- add isModelScoped400() and exempt it from the body-specific stop guard
- regression tests for wrapper forms; body-specific stop still intact
Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>
Regenerated the [3.8.49] CHANGELOG from the full cycle range (bump 2c62333b0..tip):
added 289 previously-uncovered merged PRs as categorized bullets with per-PR author
attribution, then re-injected the 🙌 Contributors hall (83 external contributors).
Closed-PR credit audit: clean — the 3 closed-not-merged authors not in the hall
(#8178 AI Council 'opened by mistake, stays on fork', #8076 Rust fetch 'closing at my
request', dependabot bot) had no landed work, so no missing credit. Synced 41 i18n
CHANGELOG mirrors.
Sync the hand-authored readme-hero.svg / promise-pillars.svg provider count
(number + accessibility desc) to 290, matching the catalog + the README/AGENTS/CLAUDE
text updated in the prior commit.
The auto-generated catalog now resolves 290 providers (grew from the tier-1/2/3
provider additions this cycle); README/AGENTS/CLAUDE still carried 278/283. Updated
all provider-count mentions + the TOC anchor/heading to 290 so check-docs-counts-sync
exits 0. Note: readme-hero.svg / promise-pillars.svg still render the old number and
need a re-render on the site-deploy machine (image render is out of gate scope).
* feat(settings): configurable model catalog cache TTL
Add modelCatalogCacheTtlMs to DatabaseSettings with default 1500ms.
Extend cache-config API route to accept the new field.
Replace hardcoded catalog cache TTL with dynamic settings value.
Add 'Cache' settings tab at /dashboard/settings/cache with sidebar entry,
i18n keys, header description, and legacy route redirect.
* feat(combo): add model connection filter toggle to ModelSelectModal
Adds a 'Show configured only' checkbox below the search bar in
ModelSelectModal that filters each provider group's models through
hasEligibleConnectionForModel. Toggle state persists in localStorage.
- Import hasEligibleConnectionForModel from domain/connectionModelRules
- showConfiguredOnly state + localStorage persistence
- connectionFilteredGroups memo layered on filteredGroups
- Renders both provider section and empty state from connectionFilteredGroups
* test(combo): add connection filter toggle tests for ModelSelectModal
Three test cases: (1) hide excluded models when toggle on,
(2) show empty state when all models excluded,
(3) drop provider group when all its models excluded.
All 3 tests pass.
* fix(settings): correct cache-config route import + add route/tab coverage
The cache-config route imported get/update helpers from a nonexistent
module (@/lib/localDb/databaseSettings) and called an undefined
updateSettings() in PUT, crashing every request. Import the real
databaseSettings module (matching the sibling database/route.ts
convention) and call updateDatabaseSettings(); idempotencyWindowMs is
routed through the flat @/lib/db/settings module instead, since that is
where it is actually read at runtime (idempotencyLayer.ts,
runtimeSettings.ts) — it was never part of the databaseSettings "cache"
section type.
Also fixes a dashboard-typecheck regression in ModelSelectModal.tsx: the
new connection-filter toggle called hasEligibleConnectionForModel() with
activeProviders entries typed too narrowly to include
providerSpecificData, which real connection objects carry at runtime.
Adds:
- tests/unit/cache-config-route-8219.test.ts: GET/PUT resolve without
crashing, modelCatalogCacheTtlMs and idempotencyWindowMs round-trip.
- tests/unit/ui/cache-settings-tab-bounds-8219.test.tsx: CacheSettingsTab
min/max TTL bounds (100ms/60000ms) gate the Save button and surface a
validation message; in-bounds values PUT correctly.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* chore(quality): rebaseline sections.ts for #8219 own-growth
---------
Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* docs(i18n): full Russian README rewrite for native readers
Rewrite docs/i18n/ru/README.md from an outdated English dump into a proper Russian manual aligned with the modern product README (v3.8.x structure, free-tier budget, combos, compression, CLI/MCP). Fix relative screenshot/doc links for the i18n path.
* fix(8217): changelog fragment bullet prefix
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* feat(providers): add Typhoon and Inception Mercury API-key providers
Two OpenAI-compatible API-key providers, each verified against a live
endpoint smoke test with a negative control before registration: an
unknown route answers 404 while /v1/chat/completions answers 401, which
rules out gateways that reply identically to every path.
- typhoon (SCB 10X, Thailand): first Thai-first provider in the catalog.
/v1/models answers 200 unauthenticated and serves exactly one chat
model, typhoon-v2.5-30b-a3b-instruct (128K ctx). The docs also list
typhoon-v2.1-12b-instruct, but the live endpoint no longer serves it,
so it is deliberately not registered. The typhoon-ocr* and typhoon-asr*
entries are OCR and speech models, not chat, and are omitted.
- inception (Inception Labs): first diffusion LLM (dLLM) in the catalog.
mercury-2 has a 128K context, 50K max output, and supports tools,
json_mode and structured outputs. The mercury-coder models advertised
on the vendor blog are no longer served by the live endpoint and are
therefore not registered.
Both expose a working /v1/models catalog, so they are added to
NAMED_OPENAI_STYLE_PROVIDERS for discovery and key validation.
Free-tier metadata is claimed only where it is documented and durable:
Typhoon issues a free API key rate-limited to 5 req/s and 200 req/m, and
Inception grants 10M tokens on signup with no card, so both are
registered with hasFree: true.
Provider count moves from 280 to 282; README/AGENTS/CLAUDE counters were
stale at 278 and are resynced against docs/reference/PROVIDER_REFERENCE.md.
* fix(8170): union frontier-labs Inception+Writer, regen ref+golden
* fix(8170): close inception object + regen ref/golden
---------
Co-authored-by: Álvaro Ángel Molina <alvaretto@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* feat(providers): add Sarvam AI, Writer Palmyra and PLaMo API-key providers
Three OpenAI-compatible API-key providers, each verified against a live
endpoint smoke test before registration:
- sarvam (India): /v1/models answers 200 unauthenticated and lists
sarvam-105b (128K ctx) and sarvam-30b (64K ctx). The older sarvam-m is
discontinued upstream and is deliberately not registered.
- writer (Palmyra): api.writer.com exposes the OpenAI alias
/v1/chat/completions alongside its native /v1/chat — confirmed with a
negative control, since an unknown route answers 404 'endpoint not
available via API gateway' while /v1/chat/completions answers 401.
Registers palmyra-x5 (1M ctx) and palmyra-x4 (128K ctx); the
medical/financial/creative/vision variants are deprecated upstream and
are omitted.
- plamo (Preferred Networks, Japan): only plamo-3.0-prime (262K ctx) is
registered. plamo-3.0-prime-beta is discontinued on 2026-07-31 and
plamo-2.2-prime on 2026-09-30, so neither is worth wiring up.
Free-tier metadata is claimed only where it is documented and durable:
Sarvam ships a permanent signup credit, while PLaMo's 10M-token grant is
a campaign that expires on 2026-07-31 and Writer documents no free tier,
so both are registered with hasFree: false.
* regen golden+ref
---------
Co-authored-by: Álvaro Ángel Molina <alvaretto@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* feat(providers): add CLOVA Studio, InternLM and Ant Ling API-key providers
Adds three OpenAI-compatible frontier-lab providers, closing regional gaps
in the catalog (Korea had none; the Shanghai AI Lab and Ant Group families
were both missing).
- clova-studio: Naver HyperCLOVA X (HCX-007 reasoning, HCX-005 multimodal)
on the current clovastudio.stream.ntruss.com host. The legacy
clovastudio.apigw.ntruss.com endpoint is being deprecated and is not used.
- internlm: Shanghai AI Lab Intern-S1 family (intern-s1-pro is a 1T MoE).
Ships a free monthly quota, so it is flagged hasFree.
- ant-ling: Ant Group / inclusionAI Ling-2.6-1T and Ring-2.6-1T.
All three endpoints were smoke-tested: each returns HTTP 401 on <baseUrl>/models
(endpoint live, awaiting auth) and resolves against public DNS. All three are
registered for live model discovery, so their catalogs refresh from upstream.
Known limitation: the ant-ling model ids are best-effort from public docs and
are NOT verified against a live /v1/models response, which requires an API key.
This is recorded in the registry comment and in the provider authHint so it is
visible to operators rather than silently assumed. Its baseUrl is likewise not
published in the public docs and was found by smoke test.
Two AI SUTRA was evaluated for this batch and deliberately excluded: its
documented endpoint api.two.ai does not resolve in public DNS (ENOTFOUND
against 1.1.1.1 and 8.8.8.8, with www.two.ai resolving as control).
The translate-path golden snapshot is regenerated; the change is additive only
(209 -> 212 keys, exactly the three new providers, none removed or altered).
* feat(providers): verify ant-ling against official docs, add Ling-2.6-flash and free tier
The ant-ling entry was added with model ids marked best-effort because they
could not be checked without an API key. Ant Ling's own documentation turns
out to publish enough to verify them, so the uncertainty is now resolved:
- The quickstart sample uses base_url "https://api.ant-ling.com/v1" with
model "Ling-2.6-1T", confirming both the endpoint and the exact casing.
- The pricing page bills exactly three models over the API, so Ling-2.6-flash
was missing from the catalog and is added.
- Each account gets 500,000 free tokens per day (resets 02:00 UTC+8, no
rollover), so the provider is flagged hasFree with a freeNote.
The Ming family (Ming-Flash-Omni, Ming-Light) is deliberately left out: it is
documented as open-source / Ling Studio only and does not appear on the
pricing page, so it is not served over this chat-completions API. That
reasoning is recorded in the registry so it is not re-litigated later.
The authHint no longer claims the ids are unverified, and now points at the
API console (https://chat.ant-ling.com/open) where keys are actually created.
Same correction applied to the en, pt-BR and vi message catalogs.
* docs: sync provider counts to 283 and regenerate the provider reference
---------
Co-authored-by: Álvaro Ángel Molina <alvaretto@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(ci): resolve upstream-inherited check failures
* fix(docs): align INPUT_SANITIZER_ENABLED default to true across all docs
Code defaults INPUT_SANITIZER_ENABLED to enabled (any value that is
not exactly 'false'). Updated .env.example and 41 i18n translations
of ENVIRONMENT.md to match this default instead of showing false.
Fixes#8093
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(ci): resolve upstream-inherited check failures
* fix(streamHandler): log trackPendingRequest decrement failures instead of swallowing
The clearPendingRequest function had an empty catch block around the
trackPendingRequest decrement call. If it threw, the pending request
counter stayed incremented — causing drift, false-positive rate limiting,
and masked overload conditions.
Now logs the error with context for observability.
Fixes#8141
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(ci): resolve upstream-inherited check failures
* fix(compression): make memo key model-independent for non-vision engines (#8137)
The compression result memo included `model` and `supportsVision` in the
cache key for ALL deterministic modes. This was correct for the `lite` engine
(which strips data:image URLs based on vision support) but unnecessary for
model-independent engines like `rtk`, `caveman`, and stacked pipelines
without a `lite` step.
In the combo retry loop, the body and config are identical across targets —
only the model changes each attempt. Including model in the key forced a fresh
cache miss on every retry, re-running the full compression pipeline 5-8x per
request instead of serving the cached result.
Fix: `makeMemoKey` now only includes model + supportsVision when the
compression pipeline actually uses a vision-dependent engine (lite, standard,
or stacked containing lite). All other deterministic engines use a
model-independent key.
- Add `usesVisionDependentEngine()` helper to classify modes
- `makeMemoKey` conditionally includes model/vision fields
- 5 new tests covering rtk, caveman, stacked-with-lite, stacked-without-lite
Closes#8137
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(ci): resolve upstream-inherited check failures
* fix(combo): skip remaining same-provider targets on 401/403 auth failure (#8133)
When a provider returns 401/403 (auth failure), every remaining model behind
the same provider will fail identically. Previously the combo engine continued
trying sibling models on the dead connection, wasting attempts.
Now auth-level failures (401, 403) are classified as provider-level exhaustion,
marking exhaustedProviders so subsequent same-provider targets are skipped via
the existing exhaustion-skip mechanism.
Tests: 4 new cases in combo-target-exhaustion.test.ts covering 401, 403,
unknown-provider guard, and per-model-quota provider (auth is provider-wide).
* fix(combo): connection-level exhaustion on 401/403, not whole-provider (#8137)
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Fix 527 mistranslated values in the Korean locale, all verified against
the en.json source:
- Restore protected product/protocol names garbled by machine translation
(응록→ngrok, 인류/인류학→Anthropic, 쌍둥이자리→Gemini, 반중력→Antigravity,
꼬리비늘 깔때기→Tailscale Funnel, 진공→VACUUM, 우편번호→ZIP)
- Fix wrong-sense homonym translations (달리기→실행 중 for Running,
장애인→비활성화됨 for Disabled, 열쇠→키 for Key, 안타→적중 for Hits,
유물→아티팩트 for Artifacts, 건강검진→상태 확인 for Healthcheck)
- Repair translated identifiers that broke literal values (양말5→socks5,
볼록-세션-id→convex-session-id, 채팅/완료→chat/completions,
메시지/보내기→message/send JSON-RPC methods)
- Replace key-name dumps shipped as values ("Table Name", "Overview
Title", "Cli Tools Redirect Title" etc.) with real Korean translations
- Unify ngrok casing (Ngrok→ngrok) and trailing punctuation with the
English source; align terminology across fixes (공급자, 폴백, 사용자 정의)
All {placeholder} tokens, markdown, and protected terms preserved
verbatim; i18n UI coverage and ko validation gates pass.
Use getEmbeddingDimension() in resolveEmbeddingSource so sqlite-vec can
create vec_memories before the first upsert, and abort reindex batches
when ensureReady returns ready=false instead of wasting embed credits.
* fix: restore OAuth auto-refresh for gemini-cli connections
gemini-cli OAuth connections had no PROVIDERS registry entry at all, so
the token-refresh health check permanently skipped them once the access
token expired, forcing a full re-authentication instead of using the
still-valid refresh token.
Two layered gaps, both required:
1. supportsTokenRefresh()'s explicit allow-set had "gemini" but not
"gemini-cli" (the id actually stored on these connections), and its
PROVIDERS[e].tokenUrl fallback also failed since...
2. ...open-sse/config/providers registry had zero entry for "gemini-cli"
at all: no clientId/clientSecret/tokenUrl/refreshUrl, so even the
generic refresh path had nothing to refresh with.
Adds a "gemini-cli" registry entry mirroring antigravity's Google
Cloud Code OAuth shape, reusing the same well-known public Gemini CLI
client credentials already embedded (and already used, unchanged, by
the Gemini Studio API-key provider's own oauth block) via
resolvePublicCred("gemini_id"/"gemini_alt"). Adds "gemini-cli" to the
explicit refresh allow-set, the Google-refresh dispatch case, and the
15-minute non-rotating-token proactive lead alongside antigravity/agy.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix: register gemini-cli in canonical provider list (provider-consistency gate)
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(gemini-cli): use ANTIGRAVITY_RUNTIME_BASE_URLS (renamed by #8013 antigravity split)
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: seanford <seanford@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
ServiceSupervisor.start(), when probeBeforeSpawn detects an already-healthy
instance on the target port, "adopts" it (marks the service running without
spawning a duplicate that would die with EADDRINUSE). This adopt path calls
setToolStatus(tool, "running") with no pid argument at all, so this.pid stays
at its constructor default of null for the rest of that supervisor's life --
only the spawn path (a genuinely new child process) ever sets a real pid.
In production this "adopt" path is common, not an edge case: any embedded
sidecar service (cliproxy, 9router, bifrost, mux) whose child process
survives a `systemctl --user restart omniroute.service` gets adopted by the
new supervisor instance on the next start(), and its pid is lost from that
point on -- even though the service is genuinely healthy and running. The
observed symptom: a service shows state "running" but pid null, and
something downstream that keys liveness tracking off pid eventually treats
it as untrustworthy/stale despite nothing actually being wrong.
Fix: resolve the real pid of the process holding the port (via `lsof -ti
:<port>`, best-effort -- a lookup failure leaves pid null rather than
blocking adoption) and record it the same way the spawn path does.
An existing test asserted pid === null on adopt. That was accurate for the
old behavior but reflected a missing resolution, not a deliberate "adopted
services never get a pid" design choice -- updated its assertion to match
the corrected behavior and added a dedicated regression test proving the
resolved pid matches the real process actually bound to the port.
* fix(sse): Gemini TPM classification, combo-cooldown-wait for auto/quota-share, and target-timeout floor
Gemini TPM/RPM 429s were misclassified as QUOTA_EXHAUSTED because
sanitizeErrorMessage() truncates to the first line, hiding Google's
metric name and retry hint on lines 2-3. Added a rawMessage field
(internal-only, never reaches the client) and classifyGeminiQuotaMetricFromText()
to classify from the untruncated text, reordered ahead of the generic
credits/daily-quota checks.
Widened comboCooldownWaitEnabled (wait out a short transient cooldown
instead of crystallizing a 429/503) from quota-share-only to also cover
auto-strategy combos, and raised the wait ceiling to 65s/130s-budget/90s-cap
to match Gemini's ~60s TPM/RPM windows.
The per-target timeout (DEFAULT_COMBO_TARGET_TIMEOUT_MS, 120s) was shorter
than the new 130s cooldown-wait budget, so a target could get cut off
mid-wait with a synthetic 524 instead of completing the retry. Added
resolveComboTargetTimeoutMsForCombo()/isComboCooldownWaitEligible() in
comboConfig.ts to raise the per-target floor to budgetMs+buffer only for
wait-eligible strategies (auto/quota-share), verified live: a 12-request
concurrent burst against a TPM-exhausted combo went from 2/12 succeeding
(10 x 524) to 12/12 succeeding with zero 503/524.
Also: liveGeminiShared.ts's sendAndValidate now fails fast on a 503
instead of retrying past it, and the health dashboard + request logger
surface TPM stats alongside RPM/RPD.
Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
* fix(sse): combo-exhausted rejection logs now capture request body + attempted models
recordRejectedRequestUsage() (the fast path for combo requests that never
reach handleChatCore, e.g. all targets locked by resilience cooldown)
hardcoded provider: "-" and never passed a request body to saveCallLog(),
so /dashboard/logs entries for these failures were nearly useless for
debugging: no way to see the client's request or which models were tried.
- recordRejectedRequestUsage() now accepts requestBody and persists it
through the existing saveCallLog() artifact mechanism (same path
handleChatCore's own logging uses).
- Added summarizeComboAttemptedModels(), which reads the combo's own model
list (always available, unlike the response's combo-diagnostics headers —
a model-level resilience-lockout skip never touches the
exhaustedProviders/exhaustedConnections sets those headers are built
from) to populate a real "provider" value instead of "-".
- Wired both into the call site in src/sse/handlers/chat.ts.
NOTE: unrelated to the Gemini TPM/combo-cooldown-wait fix on this branch —
landed here per operator request, to be split into its own branch/PR.
Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
* feat(sse): synthetic streaming keep-alive event + 5-minute Gemini cooldown-wait ceiling
Many clients enforce a first-SSE-byte timeout, which made it unsafe to wait
out a longer upstream rate-limit cooldown on a streaming request — the
client would abandon the connection before any bytes arrived. This landed
in two parts:
1. Synthetic startup "thinking" event (OpenAI chat/completions format):
the already-existing withEarlyStreamKeepalive wrapper (open-sse/utils/
earlyStreamKeepalive.ts, wired into /v1/chat/completions, /v1/messages,
/v1/responses since #2544) opens the SSE stream immediately once a
request runs past its threshold, but only ever sent empty/no-op
keepalive frames. Added a `startupFrame` option (defaults to
`keepaliveFrame` — zero behavior change unless a route opts in) so the
very first frame can carry real content instead. Wired
OPENAI_STARTUP_THINKING_FRAME (a reasoning_content delta: "OmniRoute:
got request, sending to provider") into /v1/chat/completions only —
Claude Messages and Responses API formats both require a preceding
envelope event (message_start / response.created) that a synthetic
pre-dispatch frame can't safely fabricate without risking a duplicate
envelope once the real stream arrives, so those two routes keep their
existing (safe, proven) keepalive frames unchanged.
2. Raised the "wait out a known cooldown, then retry" ceiling to 5 minutes
for both retry mechanisms, now that a client-side first-byte timeout is
no longer a risk on the (opted-in) route:
- comboCooldownWait (auto/quota-share combos, open-sse/services/combo.ts):
maxWaitMs hard clamp raised 90s -> 300s (src/lib/resilience/settings/
normalize.ts); defaults raised to maxWaitMs:90s/maxAttempts:5/
budgetMs:300s. comboConfig.ts's resolveComboTargetTimeoutMsForCombo
already derives the per-target timeout floor from budgetMs, so it
tracks the new ceiling with no further changes.
- waitForCooldown (direct, non-combo model requests, src/sse/handlers/
chat.ts): this mechanism had NO cumulative cap before — only a
per-wait cap (maxRetryWaitMs) and a retry count (maxRetries), so
maxRetries x maxRetryWaitMs could exceed 5 minutes with no ceiling.
Added a budgetMs field (mirrors comboCooldownWait) to
WaitForCooldownSettings/CooldownAwareRetrySettings, threaded a
requestRetryBudgetLeftMs tracker through chat.ts's requestAttemptLoop
(mirrors combo.ts's comboCooldownBudgetLeftMs), and made
getCooldownAwareRetryDecision refuse to wait once the cumulative
budget is exhausted even if the single wait is under maxRetryWaitMs.
Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
* fix(sse): extend the synthetic keep-alive thinking event to /v1/responses
Live incident (OpenClaw, log id 1784407081908-cbc24f): a /v1/responses
request to gemini/gemma-4-31b-it took 56s to produce a first byte and the
client disconnected (499 request_signal_aborted) — the same client-first-byte-
timeout problem the previous commit fixed for /v1/chat/completions, but
/v1/responses only had the generic bare-comment keepalive (no content), so it
wasn't covered.
Added RESPONSES_STARTUP_THINKING_FRAME: a self-contained synthetic reasoning
item (response.output_item.added -> reasoning_summary_part.added ->
reasoning_summary_text.delta -> reasoning_summary_part.done), opened AND
closed within this one frame rather than left dangling — it never carries a
response_id, so it can't collide with the real upstream response's own
independent response.created lifecycle that follows. Mirrors the abbreviated
delta+part.done close pattern open-sse/utils/stream.ts's own
emitSyntheticResponsesReasoningSummary already uses for real mid-stream
reasoning content.
Wired into src/app/api/v1/responses/route.ts via the startupFrame option
added in the previous commit.
Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
* fix(sse): combo cooldown-wait vars reset every setTry, crystallizing a bogus 503 instead of waiting
Live incident (log id 1784416706646-51): a request to the "default" combo
(strategy=auto, maxSetRetries=3) hit a real Gemini TPM 429 on both gemma-4
targets, correctly classified as a short 40s rate_limit lockout — then
crystallized a 503 "all upstream accounts are inactive" in 6.9s instead of
ever reaching the cooldown-aware wait.
Root cause: `lastError`/`earliestRetryAfter`/`lastStatus` were declared with
`let` INSIDE the `for (setTry...)` loop body, so they reset to null at the
start of every set-try. When both targets lock out on setTry 0, every
subsequent setTry (1..maxSetRetries) pre-skips both targets via the
isModelLocked check with no real dispatch — so on the FINAL setTry (the only
one whose values the post-loop decision reads, since it's gated behind
`if (setTry < maxSetRetries) continue`), lastStatus was null, hitting the
"!lastStatus" branch (ALL_ACCOUNTS_INACTIVE 503) and completely bypassing the
comboCooldownWaitEnabled / earliestRetryAfter wait logic — even though a
real 429 with a known ~40s retry-after WAS observed on setTry 0.
This bug predates today's Gemini TPM work (any combo with maxSetRetries > 0
whose targets all lock out on the first pass was affected) but was masked in
existing tests: the "auto strategy (2 models...)" regression test uses
maxSetRetries: 0, so it only ever runs ONE setTry iteration and never
exercises the reset-on-retry path. It also explains why the dedicated
12-concurrent-request burst test passed cleanly — with concurrent requests,
timing variance meant some request's FINAL setTry iteration still had a live
target to dispatch to, giving lastStatus/earliestRetryAfter fresh data. A
single isolated request has no such luck.
Fix: hoist lastError/earliestRetryAfter/lastStatus to just inside
dispatchWithCooldownRetry, before the setTry loop, so they persist across
set-tries (still reset fresh on each recursive dispatchWithCooldownRetry()
call after a wait, which is correct). recordedAttempts/fallbackCount/
exhaustedProviders etc. are intentionally left per-iteration (unrelated to
this bug).
New regression test in tests/unit/combo-quota-share-cooldown-wait.test.ts
reproduces the exact live scenario (2 targets, both lock out on setTry 0,
maxSetRetries: 3) — confirmed red (503) against the pre-fix code, green
(200, waits and retries) against the fix.
Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
* test(sse): extend live Gemini workload to Responses API + add large-context TPM test
Two additions to the live Gemini test suite, both live-verified against the
dev instance:
1. sendAndValidate() (tests/integration/liveGeminiShared.ts) now accepts an
apiFormat: "chat" | "responses" parameter, building the Responses-API
request shape (input array, max_output_tokens) and parsing its SSE events
(response.output_text.delta / response.reasoning_summary_text.delta /
response.completed) via the new readResponsesSSEStream(). Wired into two
new tests in live-gemini-workload.test.ts ([30]/[31]), mirroring the
existing Chat Completions streaming coverage. Verified live: 24/25 + 5/5
payloads succeeded end-to-end through the new code path (the one failure
was a ~300s test-client fetch timeout unrelated to the Responses API code
itself — a separate, not-yet-addressed test-harness limitation).
2. genHugeContextMessage() builds a single message large enough (~4
chars/token estimate) to approach or exceed Gemini's free-tier TPM ceiling
(16000 input tokens/min for gemma-4) by itself. Every other prompt
generator in this file tops out around 1-2k tokens — nowhere near that
ceiling — so none of the existing workload tests ever exercised a REAL TPM
429, only RPM-style rate limiting. tests/integration/gemini-large-context-tpm.test.ts
sends two ~12-13k-token requests back-to-back (comfortably exceeding
16000/min together) to exercise the full path against production Gemini:
TPM classification, the comboCooldownWait retry, and the synthetic
keep-alive frame on a genuinely slow request.
Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
* fix(sse): abandoned combo target dispatch now observes its own per-target timeout, fixing a permanent "pending" dashboard leak
Live incident (dashboard log id 1784418258231-14961a, reported as "an ongoing
request even though there's already a 200"): a combo target dispatch
abandoned by comboTargetTimeoutMs (open-sse/services/combo/targetTimeoutRunner.ts)
left a permanent phantom "pending" entry in the dashboard, even after the
overall combo request had already succeeded via a different retry.
Root cause: chatCore.ts's createStreamController — and everything downstream
that depends on it (withRateLimit's Promise.race against Bottleneck,
acquireAccountSemaphore) — only ever watches clientRawRequest.signal, which
is the ORIGINAL client's request signal (set once via buildClientRawRequest
and reused unchanged across every target dispatch in a combo). It has no
connection to targetTimeoutRunner.ts's OWN AbortController
(target.modelAbortSignal), which is what actually fires when
comboTargetTimeoutMs (300s) elapses. src/sse/handlers/chat.ts's
handleSingleModel bridge between combo.ts and handleSingleModelChat received
`target.modelAbortSignal` but silently dropped it — never forwarded it
anywhere. So when a target got abandoned (e.g. stuck inside a wedged
Bottleneck rate-limiter queue, see the WEDGED force-reset log line from the
same incident), its per-target timeout fired and let the COMBO move on and
retry successfully elsewhere — but the abandoned dispatch's own promise
chain never learned it had been superseded, so it hung forever waiting on a
signal that was never going to fire, and trackPendingRequest(false) (the
finalize call) never ran.
Fix: thread target.modelAbortSignal through as a new modelAbortSignal
runtimeOption, and merge it into clientRawRequest.signal (via the existing
mergeAbortSignals helper from open-sse/executors/base.ts) right before
dispatch, so an abandoned target's own promise chain now observes its abort
and can reach its cleanup path — new resolveDispatchClientRawRequest() makes
this mechanically testable in isolation.
Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
* fix(sse): combo cooldown-wait state recording, rate-limit wedge recovery, OpenAI-format SSE error frames
Five related fixes surfaced by live incidents (dashboard log ids 1784457764961-73,
1784465227489-a2cbc0, 1784504040241-6f8b9a) while validating the Gemini TPM/cooldown-wait
work on this branch against real OpenClaw traffic:
- combo.ts: the model-lockout bail-out branches in dispatchWithCooldownRetry never
recorded lastStatus, so once every target in a set hit an existing lockout the final
check crystallized a bogus ALL_ACCOUNTS_INACTIVE 503 instead of reaching the
cooldown-wait decision, even with a real 429 + short retry-after observed.
- combo.ts/combo/types.ts: the "all credentials cooling down" pre-dispatch rejection
(buildModelCooldownBody) nests its retry hint as error.retry_after/reset_seconds, not
the top-level retryAfter every other 429 shape uses — combo's extraction only read the
latter, so earliestRetryAfter stayed null for this shape even after lastStatus was fixed.
- rateLimitManager.ts: the wedge-recovery watchdog used disconnect(), which releases the
heartbeat timer but never rejects jobs already QUEUED on that instance — orphaned
dispatches hung until the outer ~300s per-target timeout, well past real clients'
patience. Switched to stop({ dropWaitingJobs: true }), safe because the wedge condition
already requires RUNNING===0 && EXECUTING===0.
- earlyStreamKeepalive.ts: the in-band error frame emitted after committing to a 200 SSE
stream was hardcoded to Anthropic's `event: error` convention for every route, including
the OpenAI-format ones (/v1/chat/completions, /v1/responses) where that framing is
either invisible or malformed to a plain data-line parser. Added per-route
OPENAI_CHAT_ERROR_FRAME / OPENAI_RESPONSES_ERROR_FRAME and wired them in.
- chatCore.ts: persisted a synthetic clientResponse error body even when the client had
already disconnected (AbortError) before that body was ever computed — misleading the
dashboard into showing "what the client received" for a response that was never sent.
Also: RequestLoggerDetail.tsx — Provider/Client Event Stream panes lost their collapse
toggle when StreamSection replaced the collapsible PayloadSection (692d6be80, unifying
active/finished request views) without carrying the toggle over.
Each fix has a TDD regression test with a confirmed red-before-green cycle.
Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
* test(sse): free-tier model + gemma-4 TPM-ceiling benchmark harness
Adds a live benchmark comparing free models OmniRoute exposes across
configured providers plus previously-unexercised no-auth providers
(felo-web, aihorde, opencode, duckduckgo-web — none need a connection
row, they were just never tried). Reuses liveGeminiShared.ts's SSE
parsers and CASE_BUILDERS instead of duplicating them.
Also adds a targeted TPM-stress test firing back-to-back large-context
prompts at the gemma-4-31b model across its 3 free hosts (Gemini,
NVIDIA, AI Horde) to isolate whether the documented 16k-tokens/minute
free-tier ceiling is Gemini-specific enforcement or an inherent
model property.
FORCE_TOOL_CHOICE_REQUIRED is a test-only, default-off env flag added
to liveGeminiShared.ts and live-gemini-agentic-loop.test.ts for an
earlier live A/B comparison of tool_choice: required vs unset — kept
as a reusable knob for future runs.
Co-Authored-By: Markus Hartung <markus.hartung@gmail.com>
* test(sse): benchmark for the 2026-07-22 newly-enabled provider batch
Adds NEWLY_ENABLED_MODELS to freeModelBenchmarkShared.ts (Mistral
Leanstral, OpenRouter's live "free"-tagged roster, OpenCode Zen's
current free models — refetched live from
https://opencode.ai/zen/v1/models since the static catalog had
drifted) and a dedicated workload benchmark test for them.
Co-Authored-By: Markus Hartung <markus.hartung@gmail.com>
* test(sse): sync geminiRateLimitTracker tests with e74a1722b's corrected Gemma 4 limits
e74a1722b updated geminiRateLimits.json's gemma-4-* entries from the stale
15/1500/-1 (rpm/rpd/tpm) to the real published free-tier values
16000/14400/16000, but never updated the tests asserting the old numbers.
Surfaced by running the full test:unit suite as a post-rebase sanity check.
Co-Authored-By: Markus Hartung <markus.hartream@gmail.com>
* chore(quality): file-size baseline for own-growth (#8213)
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
Co-authored-by: Markus Hartung <markus.hartream@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(sse): surface OpenRouter mid-stream error chunks instead of a false empty success
OpenRouter (and similar OpenAI-compatible aggregators) can send an HTTP 200
SSE stream whose body carries a chat.completion.chunk with empty choices and
a top-level error object instead of any delta -- e.g. the underlying
provider hitting its own capacity limit mid-request. The Responses-API
response translator's `!chunk.choices?.length` branch treated this exactly
like a legitimate trailing-usage/no-op chunk, so the stream silently ended
with response.completed / error: null / output: [] -- a false "successful
but empty" response masking a real 502 provider_unavailable failure.
Live repro: request 1784726796287-a45bb3 (OpenClaw via the default combo,
nvidia/nemotron-3-ultra-550b-a55b:free via OpenRouter) got HTTP 200 with 0
tokens in/out after Nvidia returned "Worker local total request limit
reached (33/32)" mid-stream; the client saw an empty completed response
with no indication anything failed.
Mirrors the Gemini mid-stream error fix (#4177): set state.upstreamError
from the in-band error chunk so stream.ts's existing upstreamError handling
takes over (sendCompleted emits status: "failed" with a real error object).
Co-Authored-By: Markus Hartung <markus.hartream@gmail.com>
* chore(quality): file-size baseline for own-growth (#8210)
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: Markus Hartung <markus.hartream@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(logs): stop an async EPIPE becoming an uncaughtException loop
A raw process.stderr.write into a broken pipe fails asynchronously, so the
try/catch around it never sees the failure. The stream emits 'error'; with no
listener on process.stderr Node re-throws it as an uncaughtException; the
framework's handler logs that through console.error; and the patched console
writes back into the same dead stream. That closes a self-sustaining loop.
Attach an 'error' listener to process.stdout and process.stderr. Node only
converts a stream 'error' into an uncaughtException when the emitter has no
listener, so the listener alone terminates the cycle. Measured in a spawn
harness over 1.5s: 3,387 uncaught exceptions before, 0 after.
Absorb EPIPE only. Attaching a listener otherwise makes every stream error on
those streams non-fatal process-wide, so ENOSPC, EBADF and the rest are
re-raised on a fresh stack to preserve today's crash semantics. The
accompanying test asserts that in a child process, because node:test
attributes any in-process uncaughtException to the running test.
Add a test-only reset() to undo the patched console and the listeners:
test:unit:fast runs --test-isolation=none, so leaked state would reach every
subsequent test file.
Refs #8181
* fix(logs): bound interceptor disk writes and self-heal a missing log dir
Two write-path defects in the same file, both independent of the loop itself.
writeEntry appended with no rate limit, so while the loop spun it wrote
unbounded lines to disk (4.3 GB in 90 minutes in the reported incident). Apply
the same policy #1006 established in structuredLogger -- 50 writes/sec, a 5s
dedup window, a bounded tracking map -- but scoped to `error` entries only.
That scoping is deliberate: structuredLogger applies its limiter solely to
error() and fatal(), whereas writeEntry serves all five of
log/info/warn/error/debug across ~800 non-error call sites. Capping those would
silently drop routine logging from the Console Log Viewer's file. A test
asserts non-error levels stay unlimited.
ensureDir() ran once in initConsoleInterceptor and never again, so a log
directory removed while the process was alive made every later append throw
ENOENT into a bare catch -- console file-logging then stopped permanently with
nothing surfaced anywhere. Recreate the directory and retry once, and report
the failure exactly once through the unpatched stderr so it cannot recurse
through the patched console or become a flood of its own.
Refs #8181
* fix(logs): skip raw stderr writes to a stream already known dead
error() and fatal() write with a raw process.stderr.write wrapped in
try {} catch {}. The comment on that line says the raw write exists to avoid
Next.js console patching "that triggers EPIPE loops" -- but on a broken pipe
the write fails asynchronously, so the catch never sees it, and the resulting
stream error is what ignites the loop.
Skip the write when the stream is already destroyed or ended, falling through
to the file sink as before. The listener added earlier is what breaks the
cycle; this stops the ignition point firing into a dead stream in the first
place. The existing try/catch is retained for the synchronous cases it always
covered.
The #1006 suppression policy and its call sites are untouched.
Refs #8181
* fix(logs): install the stdio guard independently of console interception
initConsoleInterceptor() returns early when APP_LOG_TO_FILE=false, and when the
log directory cannot be created. The stdio 'error' listeners were installed
after that return, so in those supported configurations no listener was
attached at all.
structuredLogger's raw stderr writes still happen there, and an ordinary broken
pipe raises an async EPIPE without destroyed or writableEnded being set first,
so the guard in safeStderrWrite does not cover it either. The loop this change
exists to prevent was therefore still reachable with file logging turned off.
Extract installStdioErrorGuard() and call it before the early return. It is
idempotent and cleared by reset(). A new test asserts, in a child process, that
both listeners are present when APP_LOG_TO_FILE=false.
Also restore APP_LOG_TO_FILE and APP_LOG_FILE_PATH in the test's after() hook.
test:unit:fast runs with --test-isolation=none, so the previous top-level
mutations leaked into later test files, leaving file logging enabled against a
path this file deletes.
Refs #8181
* fix(sse): collapse single-text-part Responses-API content to a plain string
Every /v1/responses request — even the simplest single-string input —
got 500'd by AI Horde's Aphrodite-backed facade. Root cause:
normalizeResponsesInputForChat() always wraps a plain string input as
`content: [{ type: "input_text", text: value }]` (a one-element array),
and openaiResponsesToOpenAIRequest() mapped that straight through to
`content: [{ type: "text", text: value }]` on the Chat Completions side
— an array. That's spec-valid (OpenAI's own API accepts both shapes),
but strict/naive OpenAI-compatible backends like AI Horde's only
implement the plain-string form and reject the array form outright.
A single-text-part array and a plain string are semantically
identical, so collapse is safe. Real multi-part messages (text+image,
text+file) are left untouched.
Regression test: tests/unit/openai-responses-single-text-content-string.test.ts
(RED before the fix — every collapsed-content assertion failed with an
object instead of a string; GREEN after).
Also adds a deeper AI Horde load-test suite (sequential/concurrent/
cross-model/sustained-throughput/new-capable-model-candidates) that
surfaced this bug via real live traffic after Behemoth-X-123B was
temporarily added to the "default" combo for evaluation.
Co-Authored-By: Markus Hartung <markus.hartung@gmail.com>
* fix(sse): unsupportedParams provider-level fallback for aihorde's live-discovered models
Real OpenClaw traffic against the newly-added Behemoth-X-123B combo
target kept 500ing on every attempt even after the Responses-API
content-array fix landed. The pipeline artifact showed why: `tools`
was still present, unstripped, in the request actually sent to AI
Horde's Aphrodite backend.
Root cause: `unsupportedParams: ["tools", "tool_choice",
"parallel_tool_calls"]` was only declared on the 3 models statically
listed in the aihorde registry entry (Cydonia-24B, Skyfall-31B,
google/gemma-4-31b). AI Horde uses `passthroughModels: true` — its
live worker roster changes constantly — so Behemoth-X-123B, like every
other dynamically-discovered aihorde model, had no model-specific
unsupportedParams entry, and getUnsupportedParams() returned [] for
it. But "the workers run raw text-completion backends" (no tool
calling) is true of every model AI Horde serves, not just the 3
catalogued ones.
Adds a provider-level `unsupportedParams` fallback on RegistryEntry,
checked by getUnsupportedParams() after the per-model lookup misses.
Set on the aihorde entry so it covers its entire live-discovered
roster, present and future, without needing a static per-model catalog
entry for each one.
Regression test: tests/unit/aihorde-tools-unsupported-provider-fallback.test.ts
(RED before the fix — Behemoth-X and deepseek-v4-flash both returned
[] instead of the stripped param list; GREEN after, with a control
case confirming the fallback doesn't leak to unrelated providers).
Co-Authored-By: Markus Hartung <markus.hartung@gmail.com>
* fix(sse): flatten leftover tool-call history when stripping unsupported tools
Third bug in the same AI Horde/Behemoth-X saga: even after tools/
tool_choice were correctly stripped from the live request (previous
fix), real combo traffic still 500'd. The conversation history itself
carried a prior turn's role:"assistant" tool_calls and role:"tool"
result messages, left over from before the combo failed over from a
tool-capable model (Gemini) to a non-tool-capable one (AI Horde). Its
raw completion backend doesn't understand those message shapes at all,
independent of whether live `tools` is present — confirmed by
reproducing with a role:"tool" message and NO tools param at all.
flattenToolHistory() (open-sse/utils/flattenToolHistory.ts) already
existed for exactly this, fully unit-tested — it just had zero call
sites anywhere in the request pipeline. Extracts the unsupported-params
strip into a small testable module
(open-sse/handlers/chatCore/unsupportedParamsStrip.ts, following the
existing chatCore god-file decomposition pattern e.g.
executorClientHeaders.ts) that now also flattens tool-call history
whenever "tools" was among the stripped params.
Regression test: tests/unit/chatcore-unsupported-params-strip.test.ts
(RED before the fix — the flattening test failed with the raw
tool_calls array still present; GREEN after). All 434 existing
chatcore-*.test.ts tests still pass.
Co-Authored-By: Markus Hartung <markus.hartung@gmail.com>
* fix(sse): gate tool-history flattening on unsupported, not on stripped-this-request
The previous commit's flattening only fired when "tools" was actually
present-and-stripped on THIS request. A second live reproduction
against AI Horde had no live `tools` param at all — only stale
tool_calls/tool-result messages inherited from before a combo
failover — and still 500'd, because that condition never triggered.
A model that can't do tool calling can't do it whether or not the
current request happens to carry a `tools` array. Gate on the
unsupported-params list itself (unsupported.includes("tools")) instead
of the subset that was actually present-and-deleted this time.
Regression test added to the same file (RED before — the no-live-tools
case left tool_calls/role:"tool" untouched; GREEN after). All 435
chatcore-*.test.ts still pass.
Co-Authored-By: Markus Hartung <markus.hartung@gmail.com>
* fix(sse): skip tool-incapable combo targets, error clearly on direct requests
Two complementary fixes for a model that structurally can't do tool
calling at all (e.g. AI Horde's raw completion backends) rather than
silently degrading — following up on the earlier strip/flatten fix,
which stopped the crashes but let a tool-incapable target still get
selected and return a 200 that narrates a fake tool call in prose
instead of erroring or being skipped.
1. Root cause, combo routing: getResolvedModelCapabilities()'s
`supportsTools` resolution only checked per-model registry entries,
synced capabilities, and static specs — none of which exist for a
dynamically-discovered model (AI Horde's passthroughModels roster
changes as workers come and go). It fell through to
heuristicToolCalling(), which optimistically defaults to `true` for
any unrecognized model (TOOL_CALLING_UNSUPPORTED_PATTERNS is empty).
Added a provider-level fallback reusing the same unsupportedParams
signal the request-time strip already relies on. This makes the
EXISTING filterTargetsByRequestCompatibility (comboStructure.ts) —
which already correctly excludes non-tool-capable targets when a
request requires tools — actually work for these models; no combo.ts
changes were needed, it was only ever fed bad capability data.
2. Direct/pinned requests: filterTargetsByRequestCompatibility only
protects combo routing. A direct request naming an exact
tool-incapable model has no other target to fail over to — added
checkToolCallingRequiredButUnsupported (chatCore/toolCallingRequiredCheck.ts),
gated on isCombo: false, returning a clear 400 instead of a 200 that
silently can't do what was asked.
Regression tests (both RED before, GREEN after):
- tests/unit/model-capabilities-provider-unsupported-tools.test.ts
- tests/unit/chatcore-tool-calling-required-check.test.ts
All 463 chatcore-*/model-capabilities-*.test.ts and 31 combo
compatibility-filter tests still pass.
Co-Authored-By: Markus Hartung <markus.hartung@gmail.com>
* fix(sse): correct handleChatCore return shape for the tool-calling-blocked error
handleChatCore's documented contract is `{ success, response, status,
error }`, not a raw Response — returning `new Response(...)` directly
(copied from a different early-return whose surrounding context turned
out not to share this function's top-level contract) produced "No
response is returned from route handler ... Expected a Response object
but received 'undefined'" and a bare 500 with an empty body, caught
immediately when verifying the previous commit live.
Uses createErrorResult() (already used by the adjacent
translation-failure branch a few lines up) instead of hand-building the
Response, matching the same pattern already established in this
function for early error returns.
Co-Authored-By: Markus Hartung <markus.hartung@gmail.com>
* fix(sse): scope Responses single-text-content collapse to providers that need it
The single-text-part content array -> plain string collapse (added for AI
Horde's Aphrodite facade, which 500s on the array form) was applied
unconditionally to every provider, silently breaking the standard OpenAI
array-shaped content contract that other providers and existing tests
depend on. Added RegistryEntry.requiresPlainStringContent, gated the
collapse on it (true only for aihorde), and threaded modelInfo.provider
through responsesHandler -> responsesApiHelper -> the translator so the
real /v1/responses call site can identify the provider.
Co-Authored-By: Markus Hartung <markus.hartream@gmail.com>
---------
Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
Co-authored-by: Markus Hartung <markus.hartream@gmail.com>
* fix(sse): synthesize tool_calls for Gemini's malformed function-call abort reasons
Live incident (dashboard log id 1784489701456-d8c0e9): Gemini terminates a
stream with finishReason MALFORMED_FUNCTION_CALL/UNEXPECTED_TOOL_CALL when its
own parser rejects an attempted tool call — there's no real functionCall part,
only a human-readable finishMessage. gemini-to-openai.ts passed this through
raw as finish_reason (9router#2462's fix, correctly keeping it off a clean
"stop"/Claude end_turn), but a raw "malformed_function_call" isn't one of
OpenAI's 5 documented finish_reason values, so a real OpenAI-format client
(OpenClaw) has no handling for it at all and silently never notices the turn
failed — confirmed live via tests/integration/live-gemini-workload.test.ts's
[28] streaming case after the Gemini TPM/rebase work on this branch.
Fix: synthesize a tool_calls entry (arguments carry the error code + Gemini's
finishMessage, valid JSON) and finish_reason: "tool_calls" instead, routing
the failure into the ordinary "tool call arguments didn't parse" path every
OpenAI-compatible agent loop already handles. Defers to a real tool call if
one already completed earlier in the same turn — the real call wins, no
synthetic entry piles on top of it.
Tests (TDD, each confirmed red-before-green):
- 5 new unit tests in the existing 9router#2462 regression file, covering the
synthesis itself, UNEXPECTED_TOOL_CALL, the real-tool-call-wins edge case,
and no-regression on a clean STOP.
- New fixture (tests/fixtures/translation/gemini-malformed-function-call-stream.json):
the real 6-chunk event series from the live incident, sanitized (personal
paths/URLs replaced with generic placeholders, structure preserved exactly).
- New integration test chains the real translator into the real Responses API
transformer using that same fixture, proving correct behavior on BOTH
/v1/chat/completions and /v1/responses from one shared ground-truth event
series.
Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
* fix(sse): don't drop a malformed tool-call failure when it lands beside a real one
Live incident (dashboard log id 1784589106014-2a42f8), analyzing why the
prior malformed-function-call fix (3568c7259) still wasn't reaching the
client in this case: Gemini can emit a REAL, valid functionCall AND finish
the SAME candidate with MALFORMED_FUNCTION_CALL — the model attempted
multiple tool calls in one turn (here: a real status-check call plus a
malformed "exec"+"cron" multi-call attempt), one parsed cleanly and the
other didn't.
The first fix version skipped synthesizing a failure signal whenever a real
tool call already existed (state.toolCalls.size > 0), on the assumption
that meant the model was retrying a LATER, separate attempt after an
earlier one already succeeded. That's indistinguishable, from the
translator's state, from this same-turn case — so it silently discarded
the malformed attempt's information entirely: the client saw the real call
succeed and never learned the other tool calls were attempted and rejected.
Fix: always synthesize the failure entry when a malformed abort reason is
seen, appending it alongside any real tool call rather than skipping it.
Multiple tool_calls in one response is normal, well-supported OpenAI
behavior (parallel tool calls), so this adds the failure as an additional
entry instead of replacing or hiding the real one.
Tests (TDD, confirmed red-before-green):
- Rewrote the unit test that encoded the old (wrong) assumption to assert
both the real and synthesized calls are present.
- New fixture (gemini-malformed-function-call-parallel-real-call-stream.json):
the real event series from this incident, sanitized.
- New integration tests (same file as 3568c7259's) prove both
/v1/chat/completions and /v1/responses surface both tool calls correctly
from this fixture.
Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
* feat(sse): honor tool_choice when translating OpenAI requests to Gemini
Investigating a live report that gemini-3.1-flash-lite frequently narrates
an intended tool call in plain text instead of actually emitting one
(dashboard log id 1784591483850-49c408 — 9 raw provider chunks, all plain
text, zero functionCall parts, clean finishReason STOP): body.tool_choice
was never read anywhere in the OpenAI->Gemini request translator.
result.toolConfig was unconditionally hardcoded to
{ functionCallingConfig: { mode: "VALIDATED" } } whenever tools were
present, regardless of what the caller sent. VALIDATED lets the model
respond with plain text OR a schema-validated function call at its own
discretion — it never forces a call the way OpenAI's tool_choice:
"required" (Gemini's ANY mode) does, so a caller had no way to compel a
tool call even when explicitly requesting one.
Added convertOpenAIToolChoiceToGemini(), mirroring the existing
convertOpenAIToolChoice() in openai-to-claude.ts for the same OpenAI
tool_choice shapes (string "auto"/"none"/"required", or
{type:"function",function:{name}} to force one specific tool):
- unset/"auto" -> VALIDATED (unchanged default, no regression)
- "required"/"any" -> ANY (forces a call)
- "none" -> NONE (disables function calling)
- {type:"function",...} -> ANY + allowedFunctionNames: [name]
Wired into both Gemini request paths: the direct/base translator
(openaiToGeminiBase) and the Antigravity/Cloud Code envelope
(wrapInCloudCodeEnvelope), which now reuses the base translator's already-
computed toolConfig instead of re-deriving its own hardcoded VALIDATED.
This unblocks (but does not itself resolve) the live question — a
tool_choice: "required" A/B test against gemini-3.1-flash-lite follows to
confirm ANY mode actually changes the narrate-vs-act behavior in practice.
Also updates the T11 any-budget allowlist for this file: the "any" string
comparisons (tool_choice value "any", not a TypeScript type) are the same
documented false-positive pattern already carved out for executors/base.ts.
Tests (TDD, confirmed red-before-green): 9 new unit tests covering all
tool_choice shapes on both the direct and Antigravity/Cloud Code paths,
plus the no-tools and unset-default no-regression cases.
Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
* chore(quality): file-size baseline for own-growth (#8211)
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* chore(quality): refresh stale any-suppression count for combo-routing-engine.test.ts
Rebasing onto release/v3.8.49 pulled in upstream's #8008 (prompt-cache
affinity), which added 2 more `any` usages to this test file (269 ->
271). ESLint's suppressions mechanism requires an exact count match —
any drift makes the whole file's suppression stale and reports every
violation as new. Not a violation to fix (pre-existing test-mock any
usage in an upstream commit), just an allowlist count refresh.
Co-Authored-By: Markus Hartung <markus.hartung@gmail.com>
* chore(quality): type the oauth-refresh-dedup test's connection filter instead of any
Upstream #8062 introduced this test file with an untyped `any` filter
callback param, which the strict any-budget lint rule flags as a new
violation (not a pre-existing one to allowlist). Derives the element type
from getProviderConnections' own return type instead of importing/hand-
writing it.
Co-Authored-By: Markus Hartung <markus.hartream@gmail.com>
---------
Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
Co-authored-by: Markus Hartung <markus.hartream@gmail.com>
Dependency-free (stdlib-only) libpcap/Ethernet/IPv4/TCP parser that answers
one question: does OmniRoute or the far end (Caddy, on behalf of whichever
client it's proxying) close the TCP connection first? Dashboard-level 499s
only tell us OmniRoute detected a dropped connection, not which side's FIN/
RST actually landed first -- this settles it from the raw packets.
Handles classic Ethernet and both "Linux cooked" linktypes (SLL/SLL2, what
`tcpdump -i any` produces) since rootless Podman has no host-visible bridge
interface to capture on directly -- the capture instructions in the script
document the nsenter-into-container-netns workaround.
Adds --find to grep every reassembled stream for a literal marker string --
in practice the reliable way to locate one specific request (the
x-correlation-id header isn't echoed on every hop) is dropping a fresh UUID
into an actual chat message and searching for it, then cross-referencing
the matched stream's timing against data/call_logs/<date>/*.json.
Co-authored-by: Markus Hartung <markus.hartream@gmail.com>
* feat(github): refresh Copilot model catalog
* feat(github): refresh Copilot model catalog (gpt-5.6 family)
Dropped the claude-opus-4.6 reinstatement (contradicts #7223/#2821 with no new
evidence; risks a production 400 on /v1/messages). Kept the gpt-5.6-sol/terra/luna
additions, which already exist on the Codex provider.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: backryun <backryun@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
RubyLLM (and other OpenAI-convention clients) embed strict:true/false directly
inside a function tools parameters JSON schema. Gemini/Antigravity rejects the
unrecognized keyword with a 400 ("Unknown name strict ... Cannot find field"),
the same failure class as the existing multipleOf entry. Broke every Chatwit
Captain tool-calling call routed through witdev_antigravity/gemini-*.
Reconstructed onto current release/v3.8.49 tip (preserves #8231 CIVIC_INTEGRITY
exclusion; the author's stale base showed it as a spurious revert).
Co-authored-by: Witroch4 <wital@example.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Scoped to the docs-truthfulness fix: reverted the erroneous INPUT_SANITIZER_ENABLED
default flip (flag is intentionally on-by-default per the #8093 ruling) and dropped
5 unrelated bundled changes. Keeps only the accurate SECURITY.md correction plus the
sanitizerFixtures / security-docs-truthfulness test.
Co-authored-by: rafaumeu <rafaumeu@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
The fast bucket fed every changed tests/unit file to node:test; vitest-only
subdirs (autoCombo) always fail under that runner and redden the train. The
classifier now carries the same {api,auth,…} allowlist as package.json's
test:unit (guarded by a sync test) and stops excluding ui/*.test.ts, which
test:unit does run.
Restrict the auto-combo no-auth (keyless) candidate pool to an allowlist —
opencode + felo — the only keyless backends verified to answer without any
credential on the reference egress (VPS .15). Excluded no-auth providers stay
usable via direct <alias>/<model> calls; they are just no longer auto-routed to.
Guard: tests/unit/noauth-autocombo-allowlist.test.ts.
Docs: add a "works the second you install it" free section near the top of the
README; sync the compression stack count 11 → 12 engines; document 13 env vars
(VNC browser-login knobs + VIBEPROXY_DATA_DIR) in .env.example / ENVIRONMENT.md.
Live AGY model discovery (isDiscoverableAgyModelId + filterUserCallableAntigravityModels)
composing with the #8013 antigravity discovery rewrite. Reconstructed onto the current
release tip (branch was ~977 commits behind); updated the test to the renamed version-cache
API (seedAntigravityVersionCache -> seedAntigravityIde/CliVersionCache) after the fusion.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Scopes the Antigravity 404 model-not-found lockout to the exact model (not the whole
family) so one missing bare model no longer hijacks the family cooldown, plus bare-model
autopick via resolveModelByProviderInference dedup. The thinking-signature-recovery portion
was dropped — #7899 is already fixed and merged on the release via #7906.
Co-authored-by: AndrianBalanescu <AndrianBalanescu@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix: strip internal reasoning placeholder from user-visible content (#8081)
The internal reasoning replay sentinel '(prior reasoning summary unavailable)'
can leak into user-visible assistant content when a model echoes it through
ordinary message.content / delta.content. Existing suppression only checked
reasoning_content fields and reasoning-specific events.
Changes:
- Add stripInternalReasoningPlaceholder() to reasoningPlaceholder.ts —
removes all occurrences of the sentinel and trims; returns '' when
nothing meaningful remains
- Streaming: strip in responsesTransformer.ts, openai-responses.ts, and
openai-to-claude.ts at the delta.content entry point; skip emission
entirely when only the placeholder was present
- Non-streaming: strip in responseSanitizer.ts sanitizeMessageContent()
and sanitizeResponsesMessageContent() (all three text paths)
translateText is unaffected (uses mode='translate' via plain newsClient).
The per-provider reasoning_content check remains as defense-in-depth.
* fix: skip only empty content block on reasoning-placeholder, keep finish_reason/tool_calls (#8081)
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* chore(quality): rebaseline openai-responses.ts own-growth (#8081 guard)
---------
Co-authored-by: Austin Liu <austinliu@Austins-MacBook-Air-3.local>
Co-authored-by: Probe Test <probe@example.com>
Co-authored-by: Dingding-leo <Dingding-leo@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Final catalog dedupe pass drops a generic/untyped chat-like sibling row
when a typed non-chat specialty row (audio/video/moderation/...) exists
for the same public id — closing the #4424 follow-up (whisper-1, tts-1,
omni-moderation-latest, elevenlabs/*, veo-free/*). Pure, I/O-free,
order-preserving; also removes a stray raw NUL byte that was embedded in
the dedupe key template literal (which made git render the file binary).
Restores test coverage for relative-order preservation across distinct
ids and for two distinct id-less entries never being grouped, keeping the
suite's assertion count at parity with the pre-fix baseline.
Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>
* fix(windows): add windowsHide to all child process spawns (#8131)
On Windows, child processes spawned without windowsHide: true cause
transient conhost.exe/cmd console windows to flash open. Audited all
spawn/exec/execFile/execSync/execFileSync call sites and added
windowsHide: true where missing.
Files patched:
- src/mitm/manager.ts (MITM server spawn)
- src/mitm/systemCommands.ts (sudo/system command spawn)
- src/mitm/inspector/systemProxyConfig.ts (execFile wrapper)
- src/shared/services/cliRuntime.ts (CLI spawn + npm execFileSync)
- src/lib/plugins/loader.ts (plugin host spawn)
- src/lib/providerModels/cursorAgent.ts (cursor binary spawn)
- src/lib/cloudflaredTunnel.ts (cloudflared spawn)
Unix-only call sites (shell: /bin/bash, which) are unaffected.
electron/main.js already had windowsHide: true.
* fix(windows): cover remaining spawn sites missed by #8131 windowsHide sweep
Extends the #8131 windowsHide audit to the three call sites the original
sweep missed: ServiceSupervisor.start() and processManager.startProcess()
(both spawn() embedded-service child processes), and
installers/utils.ts::buildNpmExecOptions() (the execFile() options runNpm()
uses to install services). All three now always set windowsHide: true so
no transient conhost.exe/cmd console window flashes open on Windows.
The two spawn() options objects are factored into small, pure, exported
builder functions (buildServiceSpawnOptions, buildCliproxyapiSpawnOptions)
so the regression test can assert on the constructed options directly,
since both call sites use a bare named `import { spawn } from
"node:child_process"` that ESM live-binding semantics make unmockable
without --experimental-test-module-mocks (not currently enabled repo-wide).
Bumps config/quality/file-size-baseline.json for cloudflaredTunnel.ts
934->935 (the PR's own +1 windowsHide line at the existing spawn options
object).
Co-Authored-By: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: Austin Liu <austinliu@Austins-MacBook-Air-3.local>
Co-authored-by: Probe Test <probe@example.com>
Co-authored-by: Dingding-leo <Dingding-leo@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(combos): expose synced reasoning-effort variants in Combo Builder model picker (#8072)
Synced reasoning-effort aliases (e.g. GLM-5.2-high, GLM-5.2-medium)
appear in the catalog and Playground but were missing from the Combo
Builder's inline model picker. buildModelOptions() added base synced
records but never ran appendSyncedEffortVariants().
Convert synced models with non-empty supportedThinkingEfforts into
catalog-shaped entries, run the shared appendSyncedEffortVariants
utility (preserving its effort normalization, provider exclusions,
suffix-collision handling, and naming behavior), and add any new
variant ids to the builder model map. Variants inherit the base
model's endpoints, context length, output limit, and thinking support.
* fix(combos): correct baseId derivation for synced effort variants (#8072)
appendSyncedEffortVariants sets a variant's own root field to
${baseRoot}-${tier} (still tier-suffixed), not the true base model id.
buildModelOptions() was deriving baseId from variant.root, so the lookup
into modelMap never matched and every <model>-<tier> variant silently
fell back to bare defaults instead of inheriting contextLength,
outputTokenLimit, supportedEndpoints, and supportsThinking from its
base model.
Track each variant's true base raw id directly while iterating tiers
during catalogShaped construction instead of re-deriving it from
variant.root.
Adds a regression test seeding a synced model with
supportedThinkingEfforts via replaceSyncedAvailableModelsForConnection
and asserting the resulting <model>-<tier> variants both appear and
inherit the base entry's metadata through getComboBuilderOptions().
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: Austin Liu <austinliu@Austins-MacBook-Air-3.local>
Co-authored-by: Probe Test <probe@example.com>
Co-authored-by: Dingding-leo <Dingding-leo@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(responses): close namespace round-trip for Responses-Chat translation (#7936)
The #7905 custom-tool-call path landed in release/v3.8.49 but left #7936
open: Responses namespace sub-tools were flattened to a bare leaf on the
Chat wire with no response-side closure, so Codex's adjudicator rejected
every namespace sub-tool call with `unsupported call` -- it only has a
dispatch entry for the header bits (namespace+name), no entry for the
bare leaf.
This patch closes the round-trip without mutating the Chat wire name
(alignment with #7905's bare-leaf contract and with the issue author's
proposed fix):
* request side (openai-responses.ts): keep tool.function.name as the bare
leaf, populate a side-band namespaceToolIdentityMap keyed on that leaf,
and thread it through translatedBody._toolNameMap.
* request -> response seam (chatCore.ts): extract the identity map before
dispatch and pass it through to the non-stream completion path and to
all three stream pipelines (translate openai-responses, translate
other, passthrough).
* response translator (response/openai-responses.ts): in emitToolCall
(response.output_item.added) and closeToolCall (custom_tool_call /
function_call output_item.done), call resolveRequestToolIdentity() to
rewrite the bare leaf back to {namespace,name} and emit codex-compatible
independent fields.
* passthrough (utils/stream.ts): add a response passthrough rewriter
restoreResponsesPassthroughFunctionCallIdentity that intercepts
response.output_item.added, response.output_item.done, and
response.completed and stamps the same {namespace,name} tuple.
* helper (requestToolIdentity.ts): a 20-line stateless resolver; never
parses a name.
The wire-visible Chat tool.function.name stays the bare leaf -- non-OpenAI
providers (NVIDIA, GLM, Kimi, Gemini, ...) frequently truncate or rewrite
long __-dotted names; bare leaves avoid that failure mode entirely. The
codex ResponseItem::FunctionCall schema (models.rs) declares an
independent namespace: Option<String> field and has a
function_call_deserializes_optional_namespace round-trip test, so
emitting it separately matches the codex adjudicator dispatch.
Includes 19 new test cases across 4 files:
- request-side bare-leaf wire + side-band ledger construction
- response-side tuple emit + unmapped passthrough + apply_patch exclusion
- ambiguous-leaf collision safety (entry dropped, leaf emits verbatim)
- per-request isolation between concurrent streams
- a precompiled Atlassian-style nested namespace override
* fix(responses): skip tool_search_call input items instead of 400 (#7936 addendum)
Codex 0.42+ emits `tool_search_call` (and later `tool_search_result`) input
items when the model uses the dynamic tool-search optimization. They are
metadata-only: they record that the model queried a subset of the
available tools, and carry nothing that OpenAI Chat Completions can
represent.
Without an explicit skip in openai-responses.ts, the input loop threw
Unsupported Responses API feature: input item type 'tool_search_call'
cannot be represented in Chat Completions -- and because these items
stay in the Responses API `input` for every follow-up turn, the whole
server returned 400 on EVERY subsequent /v1/responses in the same
session until the user cleared history.
Observed in the wild:
/v1/responses 400 "Unsupported Responses API feature: input item
type 'tool_search_call' cannot be represented in Chat Completions
[longcat/LongCat-2.0 (400), longcat/LongCat-2.0 (400)]"
The meituan combo (longcat fallback) was the most visible victim, but
the underlying throw is source-format-side and hits any Responses-API
consumer whose upstream does not natively support Responses.
Fix: stop on the item type the same way `reasoning` is skipped --
display-only metadata, no chat side-effect. Covers both
`tool_search_call` and the follow-up `tool_search_result` shapes.
Adds 3 unit tests:
- tool_search_call is silently skipped (no 400)
- tool_search_result is silently skipped
- tool_search_call items interspersed with real messages are skipped
in order; real messages survive
* chore(quality): rebaseline openai-responses.ts + stream.ts own-growth (#7936 namespace round-trip)
---------
Co-authored-by: TonPro <hello@tonpro.fu>
Co-authored-by: RCrushMe <RCrushMe@users.noreply.github.com>
* fix(security): decouple request PII redaction from injection mode
PII_REDACTION_ENABLED now rewrites request PII independently of
INPUT_SANITIZER_MODE, so the enterprise recipe (MODE=block + PII on)
actually redacts. Also cover Responses API string input/prompt shapes
and correct docs that claimed MODE=redact strips injection text.
Refs: #8092#8093#8094#8096#8097
* test(security): drop no-explicit-any in sanitizer unit tests
Unblocks CI lint/quality ratchet on the PII redaction PR by typing
chat-like payloads instead of casting to any.
---------
Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>
Recognize Google RESOURCE_EXHAUSTED responses that include a billing-period reset window while preserving transient rate-limit classification for generic exhaustion messages.
Fixes#8060
persistOAuthConnection gated its whole dedup step behind if(tokenData.email).
The matcher (findExistingOAuthConnectionMatch) already matches by explicit
connectionId first, but it was never reached when the payload had no top-level
email. GitHub Copilot's device-code flow keeps identity under
providerSpecificData.githubEmail, so tokenData.email is undefined — a refresh
(which passes the existing connectionId) skipped the match and fell through to
createProviderConnection, producing a duplicate connection.
- Widen the gate to if(connectionId || tokenData.email) so an explicit
connectionId is honored regardless of email.
- Guard the matcher's email branch with if(!tokenData.email) return false, so a
widened gate can't false-match an email-less connection via
safeEqual(undefined, undefined).
Fixes#8059.
Two accuracy problems in buildCodexUsageQuotas (open-sse/services/codexUsageQuotas.ts):
1. ChatGPT Codex's /wham/usage advertises a latent per-feature ceiling for the
spark feature (metered_feature codex_bengalfox) to accounts by default. A
never-used bucket is unanchored (used_percent 0, reset_after_seconds ==
limit_window_seconds), so it recomputes its reset as now + full_window on
every fetch and was rendered as a permanent GPT-5.3-Codex-Spark row at 100%
for a model the operator never used. Skip latent windows (isLatentWindow);
they reappear once the feature is actually used. The label now comes from the
payload's own limit_name, falling back to the constant.
2. primary_window/secondary_window were labeled session/weekly purely by
position, ignoring limit_window_seconds, so a 7-day primary_window showed
'Session'. The session/weekly keys (routing semantics) stay unchanged; only
the display label is corrected from the real window duration
(windowDurationLabel), so a 7-day window shows 'Weekly'.
Fixes#8051.
Point dashboard provider cards at current Baidu developer landings instead of the deprecated yiyan nag page and the 301ing wenxinworkshop path.
Co-authored-by: LandLord64 <ulofeuduokhai@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Explain release/* as the active cycle, main as the published line, and tags as ship markers so contributors know where to aim PRs.
Co-authored-by: Ulofe Uduokhai <179733406+c4usal@users.noreply.github.com>
Document the failure mode where a leftover hand-made anthropic-compatible /
openai-compatible-chat provider owns the agentrouter/<model> IDs (or is
referenced by a combo), so requests route to it instead of the built-in
agentrouter provider and get rejected with 'unauthorized client detected' or
an HTML error page. Point users at the ROUTING log line to diagnose and steer
them back to the native provider.
One-shot 提供商->提供者 normalization across src/i18n/messages/zh-CN.json
(679 substitutions) and bin/cli/locales/zh-CN.json (54), mirroring #8024's
zh-TW pass. Adds a versioned terminology glossary
(scripts/i18n/glossary/zh-CN.json), a protected-names list
(scripts/i18n/glossary/protected-terms.json), and a pure-function
consistency check (scripts/i18n/check-glossary-consistency.mjs,
npm run i18n:check-glossary) wired into CI as the i18n-glossary-zhcn job.
zh-CN added to the visual-QA harness default locales. Complements the
existing parity (check-ui-keys-coverage.mjs) and ICU (validate_translation.py)
gates without replacing them.
Pollinations image requests with no configured apiKey/accessToken (the
common free case) were sent with no Authorization header AND no
fingerprint headers, so Pollinations' own upstream legitimately
rejected them with a real 401 even for a valid OmniRoute key. The
chat path already has an anonymous fingerprint-pool fallback
(PollinationsExecutor.execute()'s isAnonymous branch); the image
path never reused it.
Adds open-sse/handlers/imageGeneration/pollinationsAnonAuth.ts,
mirroring the chat executor's anonymous session-pool fallback for
handleOpenAIImageGeneration, and fixes the pre-existing bug where
Authorization was set to the literal string "Bearer undefined"
when no token was configured (now correctly gated by if (token)).
Regression test: tests/unit/pollinations-image-anon-fallback-8085.test.ts
* fix(providers): add missing poe registry baseUrl entry (#8082)
The built-in poe provider (passthroughModels:true, NAMED_OPENAI_STYLE_PROVIDERS)
had no open-sse/config/providers/ REGISTRY entry, so model discovery's
getRegistryEntry("poe")?.baseUrl resolved to undefined and GET
/api/providers/[id]/models always failed with {"error":"No base URL
configured for provider"} even though credentials and inference worked fine
(the validation/inference path already had a hardcoded https://api.poe.com/v1
fallback). Adds a real REGISTRY entry mirroring moonshot/byteplus, and points
the audioMiscProviders.ts hardcoded fallback at the same POE_DEFAULT_BASE_URL
constant so both paths agree going forward.
* test: regenerate provider translate-path golden for poe (#8082)
src/domain/quotaCache.ts kept its quota state (cache Map, refreshingSet,
refreshTimer, tickRunning) in bare module-scope variables. In a Next.js 16
`output: "standalone"` build, code reachable only from instrumentation-node.ts
(providerLimitsSyncScheduler's write path) and code reachable from an
API-route/SSE-handler chunk (auth.ts::evaluateQuotaLimitPolicy()'s read path)
can be compiled into separate server chunks, each independently instantiating
this module's top-level state. A quota renewal written by the sync scheduler
was invisible to the routing read path, leaving accounts stuck exhausted until
a full process restart.
Anchors all quota-cache state on a single globalThis-held object, following
the same pattern already used in src/lib/credentialHealth/cache.ts and
src/lib/db/core.ts, and the identical fix already shipped for this exact
failure mode in src/lib/pricingSync.ts (#6325 / commit de9d748dac).
Regression test: tests/unit/repro-8065-quota-cache-cross-instance.test.ts
imports the module twice under distinct query-string specifiers to force two
separate module instances, proving a write from one instance is now visible
to a read from the other.
The Codex Responses-over-WebSocket bridge bypassed the whole prompt-compression
pipeline (and its analytics writes) that the HTTP/SSE path (chatCore.ts) runs on
every request, via two gaps:
1. prepare() in codex-responses-ws/route.ts never called anything from
open-sse/services/compression/* — it authenticated, injected memory, applied
reasoning-routing, then went straight to executor.transformRequest().
2. scripts/dev/responses-ws-proxy.mjs memoized the upstream connection in
ensureUpstream() and only called the internal "prepare" action on the FIRST
response.create of a WS session — every subsequent turn on a reused
connection bypassed prepare() (and therefore compression) entirely.
Fix: a new compression.ts module wires the core compression pipeline (settings
resolution -> selectCompressionStrategy -> applyCompressionAsync ->
compression_analytics/compression_engine_breakdown writes, reusing
adaptBodyForCompression's existing Responses-API input[] adapter) into
prepare(); responses-ws-proxy.mjs now re-runs prepare() (via a new shared
runPrepare() helper) for every logical response.create turn on a reused
connection, not just the first, without recreating the upstream socket.
Regression test: tests/unit/responses-ws-proxy-compression-parity.test.ts
proves the reused-connection bypass by execution (RED: 1 prepare call for 2
turns; GREEN after the fix: 2 prepare calls for 2 turns).
Windows console-window close delivers CTRL_CLOSE_EVENT, which Node/libuv maps
to a JS-visible SIGHUP event. initGracefulShutdown() only listened for
SIGTERM/SIGINT, so closing the window never ran cleanup() (WAL checkpoint +
closeDbInstance()), leaving storage.sqlite's WAL un-checkpointed for the next
launch.
Separately, process.kill(pid, "SIGTERM") on win32 unconditionally
force-terminates the target process instead of delivering an interceptable
signal. The CLI's own stop paths (ServerSupervisor.stop() and
runStopCommand()) sent it immediately on every stop, racing and beating the
child's own async graceful shutdown before the WAL checkpoint could run.
Fix:
- src/lib/gracefulShutdown.ts: register a SIGHUP handler alongside
SIGTERM/SIGINT.
- src/shared/platform/windowsProcess.ts (new): stopProcessGracefully() skips
the immediate SIGTERM on win32 (letting the target's own CTRL_C/CTRL_CLOSE
handling run) and polls before escalating to SIGKILL; unchanged immediate
SIGTERM behavior on POSIX.
- bin/cli/runtime/processSupervisor.mjs and bin/cli/commands/stop.mjs: use
stopProcessGracefully() instead of an unconditional process.kill(SIGTERM).
Regression tests: tests/unit/graceful-shutdown-sighup-8045.test.ts (reuses
the RED probe from the triage analysis) and
tests/unit/windows-process-stop-8045.test.ts.
checkRunnable() built the healthcheck spawn's minimalEnv.PATH from the
caller's PATH only, never merging in this Node's own bin dir the way
locateCommand's known-path search already does. npm-installed CLIs like
codex are `#!/usr/bin/env node` shebang scripts, so when the server is
launched with a minimal PATH (systemd/docker/PM2/Electron) lacking
node's dir, the healthcheck spawn fails even though the binary was
correctly located, and the tool shows as undetected.
Extracted the merge into a new buildHealthcheckPath() helper
(cliRuntimeHealthcheckPath.ts) to keep cliRuntime.ts within its frozen
file-size ceiling.
getDbInstance()'s probe-then-reopen pattern (written for per-open-handle
drivers like better-sqlite3/node:sqlite) was calling .close() on a
throwaway probe connection before opening the "real" connection right
after. For sql.js, openSqliteDatabase()'s fallback path always returns
the SAME module-global cached singleton for a given filePath, so closing
"the probe" closed the ONLY connection that file would ever get until
process restart — every subsequent query threw sql.js's raw "Database
closed" string, matching the reported crash-loop on every boot once
storage.sqlite already exists and both sync drivers are unavailable.
Adds closeProbeIfSafe() (src/lib/db/core.ts) and uses it at every
probe-close site in getDbInstance()/captureCriticalDbState() — it skips
the close for sql.js-backed adapters and lets the same live adapter flow
through, while still closing real per-handle drivers normally.
Also makes sqljsAdapter.ts's gracefulClose() remove its 3 process-level
listeners (beforeExit/SIGINT/SIGTERM) so a closed adapter's closure (raw
sql.js Database + buffers) can actually be garbage collected instead of
being pinned forever — addresses the compounding-OOM sub-finding as a
consequence of the same defect.
Regression test: tests/unit/db-sqljs-close-poison-7494.test.ts
* chore(ci): add .mergify.yml to main — Mergify only reads config from the default branch (#7168)
* feat(vnc-session): persistent noVNC browser login for web cookie/token providers
## Why (the headless-install problem)
OmniRoute's web cookie/token providers (ChatGPT Web, Gemini Web, Claude
Web, DeepSeek Web, …) need a live browser session, but the gateway normally
runs **headless** — as a systemd service, inside Docker, or on a VPS with no
display. There is no desktop for the operator to log into the provider in.
Today the operator has to obtain the session cookie/token *out of band* (open a
real browser elsewhere, export cookies, paste them into the connection row). That
is fiddly, breaks on every provider UI change, and is a non-starter on a
headless box where you can't open a browser at all.
This PR adds an **on-demand interactive login**: OmniRoute boots a
containerized browser that exposes a noVNC web UI at the host. The operator
opens that URL in *their own* browser, logs in normally, and OmniRoute then
harvests the resulting cookies / localStorage back into the provider's
`provider_connections` row over the DevTools Protocol. No display required on
the host — the headless server renders the login into a container and the human
just drives it through a web page.
## How we ran into this
- The shipped `dist/` bundle has **no App Router source**, so the only visible
seam was `dist/server-ws.mjs`'s `http.createServer` monkeypatch. That seam
is **dead**: Next's standalone `startServer` creates its own http server in a
way that bypasses the override, so a route registered there never fires
(debug logs confirmed: zero requests reached it). The real seam is the Next
**App Router** (`src/app/api/...`), which lives in the dev tree, not `dist/`.
- **Chromium ≥130 forces the remote-debugging port onto `127.0.0.1`** and
ignores `--remote-debugging-address=0.0.0.0`. A plain published port can't
reach it, so cookie harvest needs an in-container TCP bridge to republish the
loopback CDP onto `0.0.0.0`. We shipped that bridge, but the cleaner
default is **Firefox** (`jlesage/firefox`): its debugger binds `0.0.0.0` out
of the box, so harvest works with no bridge at all.
- The CDP harvester **hung forever** on the first tries: the message handler was
defined but never attached to the socket, so every `send()` promise stayed
pending. We replaced Playwright's `connectOverCDP` (which stalls through the
bridge) with a **raw `ws` client** and wired the handler — now resolves.
## What
New management API (scoped like the other admin endpoints via
`requireManagementAuth`):
| Method | Path | Purpose |
| --- | --- | --- |
| GET | `/api/vnc-session` | list active sessions + supported providers |
| GET | `/api/vnc-session/:provider` | session state |
| POST | `/api/vnc-session/:provider/start` | boot browser container → returns `vncUrl` |
| POST | `/api/vnc-session/:provider/harvest` | persist cookies into the provider row |
| POST | `/api/vnc-session/:provider/touch` | defer idle auto-stop |
| DELETE | `/api/vnc-session/:provider` | stop + remove the container |
## Implementation
- `src/lib/vncSession/manifest.ts` — provider → login URL + cookie/token map + config
- `src/lib/vncSession/harvest.ts` — raw-CDP cookie/localStorage harvester (`ws`)
- `src/lib/vncSession/service.ts` — docker lifecycle, port allocation, idle sweep, DB write
- `src/app/api/vnc-session/**` — App Router routes
- `src/lib/gracefulShutdown.ts` — tears down running login containers on exit
## Browser image choice
Default is **`jlesage/firefox`** (0.0.0.0-friendly CDP, no bridge). The
Chromium image + in-container bridge lives under `docker/vnc-browser/chromium`,
selectable via `OMNIROUTE_VNC_IMAGE`. See `docker/vnc-browser/README.md`.
## Config (env)
`OMNIROUTE_VNC_IMAGE`, `OMNIROUTE_VNC_CONTAINER_VNC_PORT`,
`OMNIROUTE_VNC_CONTAINER_CDP_PORT`, `OMNIROUTE_VNC_PROFILE_DIR`,
`OMNIROUTE_VNC_IDLE_MS`, `OMNIROUTE_VNC_MAX_MS`, `OMNIROUTE_VNC_MAX_SESSIONS`,
`OMNIROUTE_DOCKER_BIN` — all documented in the docker README.
## Tests
`tests/unit/vnc-session.test.ts` — manifest lookup + credential mapping
(cookie / token / whole-jar). All passing via the Node test runner.
## Notes
- Docker is the only external dependency; if the `docker` CLI is missing, `start`
throws a clear error and shutdown is a no-op.
- No secrets are returned by any endpoint — only session metadata + ports.
Co-authored-by: Sora <138304505+Capslockb@users.noreply.github.com>
Co-authored-by: Bernardo <138304505+Capslockb@users.noreply.github.com>
* refactor(vnc-session): derive provider credentials from shared contract
* fix(vnc-session): harden CDP harvesting and credential filtering
* refactor(vnc-session): scope lifecycle to provider connections
* fix(vnc-session): sanitize and scope management routes
* fix(vnc-session): use canonical provider list in API
* test(vnc-session): align coverage with canonical manifest
* docs(vnc-session): align browser setup with current implementation
* fix(security): loopback-gate /api/vnc-session (Hard Rule #15/#17)
The new /api/vnc-session/* routes spawn Docker containers via
child_process.spawn (src/lib/vncSession/service.ts) but were never
registered in LOCAL_ONLY_API_PREFIXES or SPAWN_CAPABLE_PREFIXES, so
they were reachable from non-loopback callers (any manage-scope API
key or dashboard session over a tunnel) - the same CVE class
(GHSA-fhh6-4qxv-rpqj) those constants exist to close.
Register VNC_ROUTE_PREFIX (already exported but unused in
manifest.ts) in both prefix lists, and add a regression test
asserting isLocalOnlyPath()/isLocalOnlyBypassableByManageScope()
correctly classify the new prefix.
Co-authored-by: CAPSLOCKB <138304505+Capslockb@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: Diego Rodrigues de Sa e Souza <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 <diegosouzapw@users.noreply.github.com>
* chore(dashboard): reframe Kimi partnership as "Open Source Friends"
Kimi (Moonshot AI) asked to frame the collaboration under an "Open Source
Friends" narrative instead of "Official Sponsor". Adopt "Supported by our
Open Source Friends" — it keeps the backing/support signal and the friendship
warmth — with Kimi as the founding friend, listed first. The substance is
unchanged: affiliate links, the transparency note, first-in-list placement and
the banner display window all stay exactly as they were. Only the label moves.
- README: section header "Sponsors" -> "Supported by our Open Source Friends";
badge "Official Supporter" -> "Founding Friend"; thank-you and support-line
copy reworded; official K3 banner (public/sponsors/kimi-k3-banner.png) added
full-width at the top of the section, linked with aff=omniroute.
- ProviderCard: badge/tooltip fallback strings -> founding-friend wording.
- i18n: kimiSponsorBanner.title, kimiOfficialSupporterBadge and
kimiOfficialSupporterTooltip updated across all 43 locales (en + 42
translations), dropping the sponsor framing in every language.
- Test providerCardKimiPartnerAccent aligned to the new "Founding Friend" badge.
* docs(readme): add Sponsors honor-roll for financial backers
Credit the project's GitHub Sponsors just below the Open Source Friends
section. The two public sponsors (Professor Igor Morais Vasconcelos, longtao)
are named with avatar links; the one sponsor who chose private visibility on
GitHub Sponsors is credited anonymously, without exposing their identity.
* docs(readme): generalize private-sponsor credit to 'and others'
Security-review follow-up: the per-caller namespace hash is a security boundary
(cross-tenant cache isolation), so a 32-bit FNV digest was too collision-prone —
an attacker could craft a cookie colliding into a victim's namespace. SHA-256
(128-bit prefix) makes accidental + crafted collisions infeasible.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* feat(media): Adobe Firefly image + video generation provider
Add unofficial adobe-firefly media provider with full OpenAI-compatible
image and video generation: Nano Banana / GPT Image families, Sora 2,
Veo 3.1 (standard/fast/reference), and Kling 3.0.
Supports browser session cookies (auto IMS token exchange) or direct
IMS access tokens, async submit-and-poll against Firefly 3P endpoints,
aspect-ratio and resolution controls, and multi-account web-session UX.
Chat completions are intentionally rejected (media-only surface).
Includes unit coverage for registry wiring, payload builders, auth
resolution, and mocked generate happy-paths.
* fix(adobe-firefly): clio auth, discovery fallback, credits balance
Root-cause 401 invalid token against live firefly.adobe.com captures:
generate/discovery use x-api-key + IMS client_id clio-playground-web
(not projectx_webapp). Align headers/origin, dual cookie to IMS exchange
(clio first, Express fallback), BKS poll rewrite for /jobs/result.
Models: parse POST /v2/models/discovery + static fallback catalog from
adobe/get_models.txt; expand image/video registries.
Limits: GET firefly.adobe.io/v1/credits/balance (SunbreakWebUI1) with
total/remaining + free/plan detail quotas. Clarify cookie vs JWT UX.
Unit tests: 27/27 pass.
* fix(adobe-firefly): reject guest tokens from page-only cookies
Live repro with firefly.adobe.com Cookie export: IMS check with
guest_allowed=true returns account_type=guest (no AdobeID). That token
fails generate (401 invalid token) and credits/balance (403
ErrMismatchOauthToken). guest_allowed=false needs adobelogin.com IMS
session cookies which are not present in a page-only Cookie paste.
- Detect/reject guest JWTs; clear error tells user to paste Bearer JWT
- Prefer user JWT from HAR/mixed paste; improve credential extraction
- Update web-cookie + credential UX to recommend Authorization Bearer
Unit tests 29/29.
* fix(adobe-firefly): production auth, Limits, and 408 load handling
Live validation against firefly.adobe.com + packaged VibeProxy:
Auth / credentials
- Prefer IMS user JWT (Bearer from firefly-3p); reject guest tokens from
page-only cookies with an actionable error
- Extract JWT from Bearer, access_token=, IMS sessionStorage tokenValue,
and mixed HAR pastes; prefer non-guest tokens
- Strip JWT from Cookie header (undici Headers.append crash on mixed paste)
- Keep sherlockToken → x-arp-session-id + sanitized Cookie for generate
Limits
- credits/balance → Record quotas (firefly_total / free / plan) so
providerLimits caches them (arrays were ignored)
- Allowlist adobe-firefly + firefly in USAGE_SUPPORTED + APIKEY limits
- Live: 10000 plan credits parsed end-to-end after refresh
Generate
- Browser-shaped gpt-image body (size auto, no extra top-level size)
- Exponential 408 "system under load" retries (8 attempts) with clear
client message that 408 is Adobe capacity, not invalid token
- Live: generate returns proper 408 under load; balance/models stay 200
Tests: adobe-firefly unit suite 33/33 pass.
* fix(adobe-firefly): match live capture headers; add gpt-image-2
- Do not send firefly.adobe.com Cookie to firefly-3p (wrong-origin; soft 408)
- Lift sherlockToken only into x-arp-session-id
- Poll headers match status_check.txt (Bearer + accept, no x-api-key)
- Catalog gpt-image-2 alias → upstream modelVersion "2" (GPT Image 2)
- Shorter 408 retry budget so clients fail fast with clear message
- Unit suite 34/34
* fix(adobe-firefly): always send x-arp-session-id on generate (fixes 408)
Root cause of Bearer JWT → HTTP 408 colligo "system under load":
submit only set x-arp-session-id when sherlockToken was present in a
cookie paste. JWT-only credentials never sent the header, and Adobe
soft-blocks those requests with instant 408 (x-colligo-timeout:0.0).
A/B against a real user IMS token:
- det nonce + synthetic ARP → 200
- random nonce + synthetic ARP → 200
- det nonce without ARP → 408
Match adobe2api / GPT2Image-Pro:
- buildAdobeSubmitNonce = sha256(user_id + prompt[:256])
- buildAdobeArpSessionId = base64({sid, ftr}) synthetic session
- buildAdobeSubmitHeaders always sets both headers
Live adobeFireflyGenerateImage end-to-end: submit + poll → S3 presigned URL.
* fix(adobe-firefly): drop literal cred fallbacks + type-clean tests
Addresses pre-merge review feedback on #8006:
- Removes the `|| "literal"` fallback after resolvePublicCred() in
adobeFireflyApiKey()/adobeFireflyExpressClientId()/adobeFireflyBalanceApiKey()
(open-sse/services/adobeFireflyClient.ts). resolvePublicCred() already
always returns the decoded embedded default, so the literal fallback
was dead code that reproduced the exact env-or-literal anti-pattern
docs/security/PUBLIC_CREDS.md documents as BAD (Hard Rule #11).
- Replaces the 11 `@typescript-eslint/no-explicit-any` casts in
tests/unit/adobe-firefly.test.ts with concrete types
(Record<string, unknown>, Headers, Error-narrowing on the
assert.rejects predicate), matching the pattern already used
elsewhere in this suite. `no-explicit-any` is a hard ESLint error
under tests/ in this repo.
- Freezes file-size baseline entries for the new
open-sse/services/adobeFireflyClient.ts (1958 LOC, new-file cap 800,
mirrors the qoderCli.ts precedent for a legitimately large new
provider client), open-sse/config/imageRegistry.ts (800->821, new
adobe-firefly registry entry) and the +3 LOC growth in
src/lib/usage/providerLimits.ts (1000->1003).
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>
Co-authored-by: artickc <artickc@users.noreply.github.com>
* feat(sse): add HyperAgent (hyperagent.com) unofficial web provider
Reverse-engineered from live SPA captures (hyperagent/*.txt):
Chat:
- Cookie session auth (full Cookie header)
- New thread via GET /threads/new (or POST /api/threads)
- POST /api/threads/{id}/chat with SPA feature flags + content
- SSE parse of text/session_start/session_end/done events
- Multi-turn sticky threadId + sessionId cache (history prefix + last assistant)
Models:
- Hardcoded catalog from SPA pricing map
- Pretty display names (Claude Fable 5) while wire modelId stays fable etc.
- /v1/models exposes pretty name; chat uses modelId
Limits:
- GET /api/settings/billing/usage → creditBlocks initialUsd/remainingUsd/usedUsd
- USD Credits quota for Limits page
Tests: 15/15 unit/executor-hyperagent
* fix(sse): HyperAgent execution mode + fable-latest wire model (no plan mode)
* fix(sse): document HyperAgent env vars + regenerate golden snapshot
Addresses pre-merge review feedback on #7994:
- Documents HYPERAGENT_USAGE_URL in .env.example and ENVIRONMENT.md
(OMNIROUTE_DATA_DIR was already documented via the sibling PromptQL
provider) so check-env-doc-sync.test.ts passes.
- Regenerates the provider-translate-path golden snapshot to include
the new hyperagent/ha registry entries.
- Swaps the local toNumber() helper in usage/hyperagent.ts for the
canonical @/shared/utils/numeric import (#7879 no-restricted-syntax
rule landed on the release branch after this PR was opened).
- Freezes file-size baseline entries for the new
open-sse/executors/hyperagent.ts (937 LOC, new-file cap 800) and the
+3 LOC growth in src/lib/usage/providerLimits.ts (1000->1003), both
irreducible to this PR's own provider-registration wiring.
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>
Security-review follow-up to #7900. The notion-web thread-session cache was keyed only
by Notion spaceId (space-, not user-scoped) and accepted arbitrary client-supplied thread
ids, so two users of the same space could pin/read each other's thread. Now: (1) the cache
key includes hashNotionCallerCookie(cookie) so each caller gets an isolated namespace, and
(2) readClientThreadId rejects any value that is not a well-formed Notion UUID.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(embeddings): support secure multimodal inputs
Closes#7956
* fix(embeddings): translate multimodal inputs and harden URL/base64 bounds
Reject oversize base64 before format validation to avoid Zod stack overflows,
translate canonical items to Jina modality-keyed and Gemini embedContent
contracts, and fetch HTTPS media server-side with DNS pinning before provider
submission.
Closes#7956
* fix(embeddings): pin DNS only for embedding media fetches
Default remote-image fetch keeps the previous globalThis.fetch path so
image-generation tests and callers stay mockable. Multimodal embeddings
still opt into undici DNS pinning for URL media.
* fix(embeddings): close 2 SSRF/DoS gaps in secure multimodal input (#7978)
Closes two gaps in the multimodal embedding input hardening from #7956:
1. `createPinnedFetch()` (the connection-pinning mechanism that closes the
DNS-rebinding TOCTOU window, GHSA-cmhj-wh2f-9cgx) had zero test coverage
anywhere in the repo. Writing that test surfaced a real regression: its
custom `connect.lookup` only implemented the single-address callback
form `(err, address, family)`. Node's autoSelectFamily/Happy Eyeballs
(on by default since Node 18) calls `lookup` with `{ all: true }` and
requires the array form `(err, addresses[])` — the mismatch threw
`ERR_INVALID_IP_ADDRESS` on every real pinned fetch, silently breaking
all URL-sourced multimodal embedding requests in production. Fixed by
branching on `options.all`.
2. The documented "16 MiB decoded per request" cap was enforced by the Zod
schema only for base64-sourced items; URL-sourced items were excluded,
and all up-to-32 items were fetched concurrently via `Promise.all` —
allowing ~256 MiB in memory at once (16x the documented bound). Fixed by
resolving items sequentially with a running byte budget shared across
base64 and fetched-URL sources, rejecting once the aggregate is
exhausted instead of after over-fetching.
Adds tests/unit/remote-image-fetch-pin-dns-connection.test.ts (real
loopback-server pinning tests) and a new aggregate-cap test in
tests/unit/embeddings-multimodal-7956.test.ts; both were verified to fail
against the pre-fix code before the corresponding fix was applied.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: Ravi Tharuma <RaviTharuma@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>
* fix(grok-cli): require full auth.json on OAuth paste import (#7610)
The Grok Build paste path told operators to paste only the JWT "key"
field, which creates connections with refresh_token=null that can never
auto-refresh. Require the full ~/.grok/auth.json object (with
refresh_token) in OAuthModal, and reject bare JWT pastes with a clear
error.
* fix(grok-cli): add behavioral test coverage for auth.json paste-import (#7610)
Replace the source-regex-only test for the OAuth paste-import path with a
real behavioral suite (bare JWT rejected, auth.json missing refresh_token
rejected, multi-entry auth.json accepted, valid auth.json POSTed) using the
existing grok-device-oauth-modal.test.tsx jsdom harness. Extract
parseGrokCliPasteToken() into its own src/lib/oauth/utils/grokCliAuthJson.ts
module so it is directly unit-testable and to keep OAuthModal.tsx's frozen
file-size gate from growing (bump 1080->1100, justified in
file-size-baseline.json, mirroring the existing extraction precedent on
this file). Also fixes two pre-existing "JWT Token" label assertions that
this PR's own tab rename ("Import auth.json") had left stale.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: Ravi Tharuma <RaviTharuma@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>
* fix(grok-cli): sanitize function_call_output before Grok Build dispatch (#7611)
Grok Build cli-chat-proxy rejects Responses bodies when tool-result
outputs contain incomplete \u escapes or other malformed JSON text.
Sanitize function_call_output.output values in GrokCliExecutor so
large agent tool transcripts no longer fail intermittently with 400
body-parse errors.
* fix(grok-cli): type test credentials instead of casting through any (#7611)
tests/ has no-explicit-any as an ESLint error; the 3 `{ accessToken: "tok" }
as any` casts passed to transformRequest() were the proven, non-drift cause
of this PR's own "No new ESLint warnings" CI failure. Replace them with a
single properly-typed ProviderCredentials literal (all fields on that type
are optional, so no cast is needed).
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: Ravi Tharuma <RaviTharuma@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>
* Add Responses tool-output compression engine
* fix: enable Codex Responses stacked steps
* fix(compression): share Codex tokenizer and rebase UI
* fix(compression): sync MCP engine selection
* fix(compression): i18n parity for codex-responses mode + rebaseline
The codex-responses compression engine already imports the shared
countTextTokens/resolveTokenizerEncoding from tiktokenCounter.ts (no
duplicate encoder) and CompressionSettingsTab.tsx already threads the
new mode through the existing useTranslations()/labelKey pattern - both
pre-existing on this branch tip after rebasing onto release/v3.8.49.
What was missing after the rebase: the new compressionModeCodexResponses
/ compressionModeCodexResponsesDesc keys existed only in en.json. Filled
en-fallback into all 42 locales via scripts/i18n/fill-missing-from-en.mjs
and added real pt-BR/vi translations. Also rebaselined the three files
whose own growth (new codex-responses mode wiring) crossed the frozen
file-size caps: open-sse/mcp-server/schemas/tools.ts, open-sse/services/
compression/strategySelector.ts, and src/lib/db/compression.ts.
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>
* fix(notion-web): reuse threadId across OpenAI multi-turn (no new chat each request)
Root cause: every execute() minted a random threadId with createThread:true,
so each OpenAI messages[] turn became a brand-new Notion AI chat. That broke
multi-turn agent flows (tool result follow-ups looked like cold starts).
- History-keyed in-memory session cache (spaceId + conversation prefix hash)
- First user turn: createThread true + new UUID
- Follow-up with prior turns: createThread false + same threadId
- Optional client continuity: body.notion_thread_id / X-Notion-Thread-Id
- Echo thread id on chat.completion (notion_thread_id + response header)
- Also accept OpenAI content-parts arrays for message content
- Unit tests: 34/34 (session lookup/store + createThread false on turn 2)
* fix(notion-web): read X-Notion-Thread-Id from clientHeaders
ExecuteInput exposes client request headers as clientHeaders, not headers.
input.headers was always undefined so client-supplied thread pins were ignored.
* fix(notion-web): prefer clientHeaders with defensive headers fallback
* fix(notion-web): sticky threads on errors + partial follow-ups
- Bind conversation root (first user) to a threadId *before* upstream call so
temporarily-unavailable / empty replies never mint a new Notion chat on retry
- Persist sticky map under DATA_DIR so multi-turn survives process restarts
- Follow-ups use createThread:false, isPartialTranscript:true, and only the
steps after the last assistant (full re-transcript was overloading Notion)
- Detect in-band Notion error objects (subType temporarily-unavailable) and
retry once with the same threadId
- Keep custom-agent workflowId support and clientHeaders thread pin
* refactor(notion-web): split thread-session/stream-parser/transcript-builder into services
The merged notion-web.ts (1490 lines) and its test file (1000 lines) tripped
the file-size gate (cap 800 for new/uncapped files). Extract three
self-contained pieces into open-sse/services/, no behavior change:
- notionThreadSessions.ts: sticky thread-session cache, disk persistence,
conversation hashing, client thread-id pin (body/header)
- notionStreamParser.ts: NDJSON runInferenceTranscript response parsing +
in-band upstream error detection
- notionTranscriptBuilder.ts: config/context/message-step transcript building
Split the corresponding "Notion thread session continuity" describe block
into tests/unit/executor-notion-web-thread-sessions.test.ts. All symbols
previously reachable via the notion-web.ts namespace import stay reachable
(re-exported) so existing test destructuring is unaffected. 44/44 tests pass.
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: Artur <artur@local>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouzapw@users.noreply.github.com>
* feat: provider tab account search + mirrored top pagination (#7937)
Two client-side UI improvements to the provider connections/accounts list
(all data already loaded in memory; PAGE_SIZE=50):
- Mirror the pagination bar ABOVE the list (previously bottom-only) in both
the flat/untagged branch and the tagged/grouped branch.
- Add a case-insensitive substring account search input (id/tag/name/email)
to the left of the status filter pills, searching across ALL accounts
(not just the current page), resetting pagination to page 0 on change.
- Add pagination to the tagged/grouped view, which previously had none.
New pure helper `connectionsSearchFilter.ts` keeps the substring matcher
testable and out of the already-large ConnectionsListPanel.tsx.
Closes#7937
* i18n(vi): add providers.accountSearchPlaceholder for locale parity (#7937)
* 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)
* docs: document npm install ERESOLVE/peer/deprecated warnings as harmless (fixes#7951)
* docs: fix Docker IPv6 connection reset with -p 127.0.0.1 bind (fixes#7722)
Docker -p 20128:20128 publishes on both IPv4 and IPv6, but the
container listens on IPv4 only. On hosts where localhost
resolves to ::1 first, connections get reset.
Changes:
- README: use -p 127.0.0.1:20128:20128 to force IPv4 bind
- TROUBLESHOOTING: add quick-fix table entry + Docker IPv6 section
with curl -4 diagnostic and permanent fix
* docs: also update guides/TROUBLESHOOTING.md with Docker IPv6 fix
---------
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: Austin Liu <austinliu@Austins-MacBook-Air-3.local>
Co-authored-by: Dingding-leo <Dingding-leo@users.noreply.github.com>
Complete the translation catalogs for the 40 locales covered by this PR and rebase them onto the current release/v3.8.49 tip.
Translate the 9 Kimi sponsor and preset keys introduced by #8039. Leave en.json, vi.json, and zh-TW.json untouched so #8024 remains authoritative for Traditional Chinese.
The UI-key coverage gate reports 100% for all 40 touched locales with no missing keys or placeholders.
Co-authored-by: nguyenha935 <208228297+nguyenha935@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* 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)
* docs: document npm install ERESOLVE/peer/deprecated warnings as harmless (fixes#7951)
* docs: document harmless npm install warnings (correct fabricated package claims) (#7951)
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
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: Austin Liu <austinliu@Austins-MacBook-Air-3.local>
Co-authored-by: Dingding-leo <Dingding-leo@users.noreply.github.com>
abort(reason) rejects the upstream fetch with the raw reason, which is
often a bare string ("request_signal_aborted", "Client disconnected: ...")
carrying no `name` or `status`. The chatCore catch block only recognized
`error.name === "AbortError"`, so those aborts fell through to the 502
provider-failure default and were surfaced as `FAILED 502 / Bad Gateway`
in the client response, request logs, and usage records.
Classify the caught error with the existing isLocalStreamLifecycleError
helper (expanded by #7908 to cover AbortError plus the known abort reason
strings) so every client-abort shape maps to `499 Request aborted`.
Status-normalization follow-up to #7908, which already excluded these
aborts from provider circuit-breaker and cooldown accounting.
Co-authored-by: xiaolong.835 <xiaolong.835@bytedance.com>
* feat: narrow mcp:connect scope + per-key HTTP tool-scope binding (#7895)
Adds MCP_CONNECT_SCOPE ("mcp:connect"), a narrow additive API-key scope
(kept out of MANAGEMENT_API_KEY_SCOPES, same precedent as SELF_USAGE_SCOPE)
that authorizes ONLY the /api/mcp/ LOCAL_ONLY route-guard carve-out --
remote MCP-only callers no longer need broad manage/admin scope just to
reach the transport routes. Scoped strictly to /api/mcp/; every other
LOCAL_ONLY bypass prefix still requires hasManageScope() unchanged.
Also resolves the caller's real api_keys.scopes over HTTP/SSE
(httpAuthContext.ts::resolveMcpCallerAuthInfo) and passes it to the MCP
SDK's transport.handleRequest(req, { authInfo }), so extra.authInfo.scopes
reaching tool calls reflects the Bearer key's own scopes instead of the
OMNIROUTE_MCP_SCOPES env fallback -- scopeEnforcement.ts already prioritized
authInfo, it was simply unfed over HTTP. Does not flip the
OMNIROUTE_MCP_ENFORCE_SCOPES default; stdio is unaffected (no per-caller
identity, stays on the meta/env fallback chain).
Closes#7895
* test(mcp): register mcp-connect-scope test in stryker tap.testFiles (#7895)
## Problem
When Gemini Flash returns a safety-filtered response (finish_reason:
content_filter, empty content), isEmptyContentResponse() misclassifies
it as a fake-success empty response and returns HTTP 502. This triggers
the combo fallback chain and account cooldown escalation (5s → 10s →
20s → 40s), even though the response is a legitimate terminal state.
## Root cause
errorClassifier.ts line 14: LEGIT_EMPTY_OPENAI_FINISH only exempts
"length" and "tool_calls". The "content_filter" finish reason
(mapped from Gemini's SAFETY/PROHIBITED_CONTENT) is not exempted,
so safety-filtered responses are treated as empty content failures.
## Fix
Add "content_filter" to LEGIT_EMPTY_OPENAI_FINISH so safety-filtered
responses pass through as valid (though filtered) completions.
## Testing
- 10/10 unit tests pass (empty-content-stopreason-3572.test.ts)
including 2 new content_filter test cases
- E2E: hot-patched OmniRoute v3.8.48 on X500, verified the previously
failing prompt (4.6KB review) now returns valid content instead of
empty-content 502
Signed-off-by: Minxi Hou <houminxi@gmail.com>
#7734 added hailuo-web to WEB_COOKIE_PROVIDERS but not to WEB_SESSION_CREDENTIAL_REQUIREMENTS,
so web-session-credentials.test.ts failed the merge-train test:unit gate.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Two base-reds on the v3.8.49 tip from already-merged PRs, both blocking the merge-train
test:unit gate:
- #7528 WS rewrite dropped the ecto_1_sess cookie hint from the 401 message (guard #5449).
- #7734 (hailuo) + #7997 (M365 variants) grew provider count to 278; docs still said 271.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
duckduckgo-web returned 400 ERR_BAD_REQUEST on every request because the model
catalog advertised ids DuckDuckGo has retired from the free Duck.ai lineup
(gpt-4o-mini, gpt-5-mini, llama-4-scout, mistral-small-2501, o3-mini,
claude-3-5-haiku-20241022). duckchat/v1/chat validates `model` server-side and
rejects retired ids, and normalizeDuckDuckGoModel() defaulted to / passed through
gpt-4o-mini, so the retired id reached the wire verbatim.
Update all three id sources to the current free wire ids captured live from
duckchat/v1/models (2026-07-22): gpt-5.4-mini, gpt-5.4-nano, claude-haiku-4-5,
mistral-small-2603, tinfoil/gpt-oss-120b, tinfoil/gemma4-31b —
- executor: default gpt-4o-mini -> gpt-5.4-mini; legacy ids aliased to the
nearest current model via a lookup map; dropped the invalid gpt-5-mini
"minimal" reasoningEffort;
- freeModelCatalog.data.ts + providers/registry/duckduckgo-web: current ids.
Regression test duckduckgo-web-model-catalog-8000.test.ts asserts no retired id
ever reaches the wire and all three catalogs match the current set (RED on the
old default/passthrough + retired catalogs). Live 200 confirmation remains a
recommended VPS smoke per the plan-file.
* feat(oauth): add browser login for Grok Build provider (#7013)
* feat(oauth): grok-build supports device_code AND browser-PKCE side-by-side (#7013)
Reworks #7735 so the browser PKCE login is added ALONGSIDE the device_code flow
(#7358) instead of replacing it; the OAuthModal lets the user pick either method.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
The KimiSponsorBanner (use client) version gate imported isNewer/normalizeVersion
from versionCheck.ts, whose top-level 'import { execFile } from child_process'
cannot be tree-shaken out of a client bundle — Turbopack next build failed with 33
'Module not found' errors (child_process, fs, net, dns, module). Move the pure
helpers to a dependency-free versionCompare.ts; versionCheck.ts re-exports them.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-22 00:07:19 -03:00
1144 changed files with 304312 additions and 61432 deletions
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
- 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.
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.
"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.",
- **feat(api):** route Google AI Studio Imagen through /v1/images/generations ([#7656](https://github.com/diegosouzapw/OmniRoute/pull/7656)) — thanks @danscMax
- **feat(providers):** notion-web live model discovery via getAvailableModels ([#7696](https://github.com/diegosouzapw/OmniRoute/pull/7696)) — thanks @artickc
- **feat(providers):** add proactive cf_clearance/User-Agent hint to grok-web connection dialog (#7567) ([#7713](https://github.com/diegosouzapw/OmniRoute/pull/7713))
- **feat:** add live gRPC-web quota fetcher for grok-cli (#6844) ([#7714](https://github.com/diegosouzapw/OmniRoute/pull/7714))
- **feat(dashboard):** show proxy name in badge, sort saved-proxy picker, default to Saved tab (#7643) ([#7720](https://github.com/diegosouzapw/OmniRoute/pull/7720))
- **feat(providers):** zai-web live model discovery with local-catalog fallback (#7678) ([#7766](https://github.com/diegosouzapw/OmniRoute/pull/7766))
- **feat(api):** sync upstream reasoning.supported_efforts into synced-model catalog (#7694) ([#7767](https://github.com/diegosouzapw/OmniRoute/pull/7767))
- **feat(dashboard):** pin Kimi providers first in category + official supporter card accent ([#7775](https://github.com/diegosouzapw/OmniRoute/pull/7775))
- **feat(perf):** IC2 — cache provider connections by ID + lazy-decrypt credentials ([#7787](https://github.com/diegosouzapw/OmniRoute/pull/7787)) — thanks @oyi77
- **feat(quality):** gate the free-tier headline so it can never silently drift again ([#7798](https://github.com/diegosouzapw/OmniRoute/pull/7798))
- **feat(providers):** expose an explicit tier override for any provider connection (#7818) ([#7838](https://github.com/diegosouzapw/OmniRoute/pull/7838))
- **feat(dashboard):** Kimi sponsor banner, Kimi Coding preset, official logomarks and partner links ([#8039](https://github.com/diegosouzapw/OmniRoute/pull/8039))
- **feat(dashboard):** make Codex quota card windows reflect reality ([#8054](https://github.com/diegosouzapw/OmniRoute/pull/8054)) — thanks @insoln
- **feat(compression):** teach the model the CCR retrieve protocol on first marker (#8033) ([#8063](https://github.com/diegosouzapw/OmniRoute/pull/8063))
- **feat(sse):** restrict auto-combo no-auth pool to allowlist (opencode, felo) + docs ([#8183](https://github.com/diegosouzapw/OmniRoute/pull/8183))
- **feat(sre):** add tcp-close-analyzer.py for debugging client-vs-server TCP close order ([#8208](https://github.com/diegosouzapw/OmniRoute/pull/8208)) — thanks @hartmark
- **fix(sse):** start credential-health sweep at boot so stale web sessions recover ([#7689](https://github.com/diegosouzapw/OmniRoute/pull/7689)) — thanks @danscMax
- **fix(cursor):** discover models via official CLI command ([#7692](https://github.com/diegosouzapw/OmniRoute/pull/7692)) — thanks @makcimbx
- **fix(claude-web):** unify Turnstile/executor/fast-path User-Agents behind one fingerprint (#7548) ([#7711](https://github.com/diegosouzapw/OmniRoute/pull/7711))
- **fix(sse):** authenticate CLIProxyAPI fallback/passthrough legs with a dedicated credential (#7645) ([#7712](https://github.com/diegosouzapw/OmniRoute/pull/7712))
- **fix(providers):** classify ambiguous Mistral 401 instead of hard auth error (#7638) ([#7718](https://github.com/diegosouzapw/OmniRoute/pull/7718))
- **fix(security):** bump adm-zip >=0.6.0 + exact host matching in mitm DNS test ([#7732](https://github.com/diegosouzapw/OmniRoute/pull/7732))
- **fix(icons):** fall back to Stepfun Mono when Color component is absent … ([#7743](https://github.com/diegosouzapw/OmniRoute/pull/7743)) — thanks @Dan-ex-hub
- **fix(stream):** suppress `</think>` close marker for Responses API clients ([#7747](https://github.com/diegosouzapw/OmniRoute/pull/7747)) — thanks @xz-dev
- **fix(sse):** wire settings.wildcardAliases into model resolution (#7693) ([#7748](https://github.com/diegosouzapw/OmniRoute/pull/7748))
- **fix(cli):** load DATA_DIR/server.env as fallback for .env on Electron migration (#7302) ([#7759](https://github.com/diegosouzapw/OmniRoute/pull/7759))
- **fix(cli):** split outboundUrlGuard's DB helpers so setup-opencode packages cleanly (#7682) ([#7760](https://github.com/diegosouzapw/OmniRoute/pull/7760))
- **fix(quality):** register nvidia-quota-phase1 and service-provider-plugin-registry in stryker tap.testFiles ([#7796](https://github.com/diegosouzapw/OmniRoute/pull/7796))
- **fix(stream):** synthesize terminal finish_reason chunk when upstream omits it (#7800) ([#7804](https://github.com/diegosouzapw/OmniRoute/pull/7804)) — thanks @AndrianBalanescu
- **fix(plugins):** 5 bugs on the plugin path (3 Windows-only, 2 all-platform) ([#7806](https://github.com/diegosouzapw/OmniRoute/pull/7806)) — thanks @tmone
- **fix(cli):** register ESM alias resolver for @/ paths under global install ([#7808](https://github.com/diegosouzapw/OmniRoute/pull/7808)) — thanks @rafaumeu
- **fix(auth):** gate invalid-key check on isRequireApiKeyEnabled for embeddings and web-fetch (#7785) ([#7810](https://github.com/diegosouzapw/OmniRoute/pull/7810)) — thanks @AndrianBalanescu
- **fix(db):** purge in-memory key-health state when a provider connection is deleted (#7740) ([#7826](https://github.com/diegosouzapw/OmniRoute/pull/7826))
- **fix(compression):** keep a retrievable preamble instead of a bare CCR marker (#7746) ([#7827](https://github.com/diegosouzapw/OmniRoute/pull/7827))
- **fix(docs):** document CREDENTIAL_REDACTION_ENABLED and GHE_COPILOT_OAUTH_CLIENT_ID (#7793) ([#7833](https://github.com/diegosouzapw/OmniRoute/pull/7833))
- **fix(dashboard):** fix collapsed quota card session/weekly order (#7764) ([#7834](https://github.com/diegosouzapw/OmniRoute/pull/7834))
- **fix(dashboard):** mirror connection-row action-icon spacing under RTL (#7680) ([#7835](https://github.com/diegosouzapw/OmniRoute/pull/7835))
- **fix:** avoid cmd.exe spawn on Windows by using os.hostname() before execSync fallback ([#7841](https://github.com/diegosouzapw/OmniRoute/pull/7841)) — thanks @tientien17
- **fix(cli):** use rundll32 instead of cmd.exe for Windows browser fallback in dashboard command ([#7844](https://github.com/diegosouzapw/OmniRoute/pull/7844)) — thanks @tientien17
- **fix(rerank):** honor the connection's pinned proxy on rerank calls (#7350) ([#7867](https://github.com/diegosouzapw/OmniRoute/pull/7867))
- **fix(compression):** skip CCR on tool outputs to preserve agent loop ([#7869](https://github.com/diegosouzapw/OmniRoute/pull/7869)) — thanks @herjarsa
- **fix(vision-bridge):** reroute auto/ prefix to vision model when images present ([#7871](https://github.com/diegosouzapw/OmniRoute/pull/7871)) — thanks @herjarsa
- **fix(opencode-plugin):** support separate management read token ([#7885](https://github.com/diegosouzapw/OmniRoute/pull/7885)) — thanks @RaviTharuma
- **fix(notion-web):** reuse threadId across OpenAI multi-turn (no new chat each request) ([#7900](https://github.com/diegosouzapw/OmniRoute/pull/7900)) — thanks @artickc
- **fix(resilience):** don't cool down accounts or trip the breaker on client aborts ([#7908](https://github.com/diegosouzapw/OmniRoute/pull/7908)) — thanks @insoln
- **fix(providers):** treat public-host 302 as valid in Gemini Web connection test (#7859) ([#7917](https://github.com/diegosouzapw/OmniRoute/pull/7917))
- **fix(providers):** read reasoning_text in Claude-format response translator (#7856) ([#7919](https://github.com/diegosouzapw/OmniRoute/pull/7919))
- **fix(ccr):** resolve principal via OMNIROUTE_API_KEY env var on stdio MCP transport ([#7932](https://github.com/diegosouzapw/OmniRoute/pull/7932)) — thanks @ekinnee
- **fix(sse):** preserve parallel_tool_calls for GPT-5.6 delegation under Codex Responses Lite (#7821) ([#7957](https://github.com/diegosouzapw/OmniRoute/pull/7957))
- **fix(providers):** treat unreliable web-cookie /models probe status as unsupported, not valid (#7857) ([#7959](https://github.com/diegosouzapw/OmniRoute/pull/7959))
- **fix(api):** add amazon-q to the static model catalog (#7820) ([#7960](https://github.com/diegosouzapw/OmniRoute/pull/7960))
- **fix:** parse Gemini 429 RetryInfo.retryDelay for model lockout (#7940) ([#7961](https://github.com/diegosouzapw/OmniRoute/pull/7961))
- **fix(quality):** tolerate ESLint's trailing unpruned-suppressions text in validate-release-green (#7837) ([#7962](https://github.com/diegosouzapw/OmniRoute/pull/7962))
- **fix(embeddings):** support secure multimodal inputs ([#7978](https://github.com/diegosouzapw/OmniRoute/pull/7978)) — thanks @RaviTharuma
- **fix(resilience):** cap exactCooldownMs against maxCooldownMs (#7940) ([#7980](https://github.com/diegosouzapw/OmniRoute/pull/7980)) — thanks @ekinnee
- **fix(electron):** derive macOS Helper name from execPath to remove 2nd Dock icon (#7941) ([#8002](https://github.com/diegosouzapw/OmniRoute/pull/8002))
- **fix(chatcore):** report string-reason client aborts as 499, not 502 (#7907) ([#8011](https://github.com/diegosouzapw/OmniRoute/pull/8011)) — thanks @Long-Feeds
- **fix(models):** drop generic catalog siblings of specialty surfaces (#8015) ([#8021](https://github.com/diegosouzapw/OmniRoute/pull/8021)) — thanks @RaviTharuma
- **fix(stream):** add logging to empty catch blocks in stream error handling ([#8143](https://github.com/diegosouzapw/OmniRoute/pull/8143)) — thanks @chirag127
- **fix(sse):** stop Codex/Responses sanitizer turning system image_url into output_text (#8089) ([#8147](https://github.com/diegosouzapw/OmniRoute/pull/8147))
- **fix(db):** register SIGHUP handler and stop force-killing server on win32 stop paths (#8045) ([#8148](https://github.com/diegosouzapw/OmniRoute/pull/8148))
- **fix(routing):** anchor quota cache on globalThis for cross-chunk consistency (#8065) ([#8150](https://github.com/diegosouzapw/OmniRoute/pull/8150))
- **fix(responses):** close namespace round-trip for Responses-Chat translation (#7936) ([#8151](https://github.com/diegosouzapw/OmniRoute/pull/8151)) — thanks @RCrushMe
- **fix(oauth):** warn instead of silently opening unreachable localhost redirect for LAN-IP Codex/xAI/Grok OAuth (#8046) ([#8152](https://github.com/diegosouzapw/OmniRoute/pull/8152))
- **fix(db):** stop closing the sql.js singleton in getDbInstance() probe/reopen (#7494) ([#8153](https://github.com/diegosouzapw/OmniRoute/pull/8153))
- **fix(sse):** run compression pipeline per turn in Codex Responses WS bridge (#8052) ([#8154](https://github.com/diegosouzapw/OmniRoute/pull/8154))
- **fix(cli):** merge node bin dir into CLI healthcheck PATH for codex detection (#8036) ([#8156](https://github.com/diegosouzapw/OmniRoute/pull/8156))
- **fix(sse):** anonymous fingerprint fallback for keyless Pollinations image gen (#8085) ([#8157](https://github.com/diegosouzapw/OmniRoute/pull/8157))
- **fix(cli):** surface the real spawn error in process supervisor (#8091) ([#8158](https://github.com/diegosouzapw/OmniRoute/pull/8158))
- **fix(combos):** expose synced reasoning-effort variants in Combo Builder model picker (#8072) ([#8165](https://github.com/diegosouzapw/OmniRoute/pull/8165)) — thanks @Dingding-leo
- **fix(windows):** add windowsHide to all child process spawns (#8131) ([#8167](https://github.com/diegosouzapw/OmniRoute/pull/8167)) — thanks @Dingding-leo
- **fix(#8093):** align INPUT_SANITIZER_ENABLED default to true across all docs ([#8185](https://github.com/diegosouzapw/OmniRoute/pull/8185)) — thanks @rafaumeu
- **fix(compression):** make memo key model-independent for non-vision engines ([#8196](https://github.com/diegosouzapw/OmniRoute/pull/8196)) — thanks @rafaumeu
- **fix(resilience):** add max/step to NumberField for provider cooldown inputs (#8107) ([#8203](https://github.com/diegosouzapw/OmniRoute/pull/8203)) — thanks @rafaumeu
- **fix(providers):** fix Azure AI Foundry multi-model discovery and per-deployment connection testing (#8174) ([#8206](https://github.com/diegosouzapw/OmniRoute/pull/8206)) — thanks @not-knope
- **fix(logs):** stop the async-EPIPE log-flood loop at its ignition point ([#8207](https://github.com/diegosouzapw/OmniRoute/pull/8207)) — thanks @Tasogarre
- **fix(sse):** surface OpenRouter mid-stream error chunks instead of a false empty success ([#8210](https://github.com/diegosouzapw/OmniRoute/pull/8210)) — thanks @hartmark
- **fix(services):** resolve and record a real pid when adopting a service ([#8218](https://github.com/diegosouzapw/OmniRoute/pull/8218)) — thanks @seanford
- **fix(api):** narrow claudeClassifierCompat auto trigger so stop_sequences alone no longer short-circuits (#8189) ([#8236](https://github.com/diegosouzapw/OmniRoute/pull/8236))
- **fix(gemini):** drop HARM_CATEGORY_CIVIC_INTEGRITY from the default Gemini safety settings (#8231) ([#8238](https://github.com/diegosouzapw/OmniRoute/pull/8238))
- **fix(backend):** word-boundary-safe tool-result truncation in lite compression mode (#8169) ([#8239](https://github.com/diegosouzapw/OmniRoute/pull/8239))
- **fix(providers):** filter unsupported family-fallback candidates against the provider catalog (#8134) ([#8240](https://github.com/diegosouzapw/OmniRoute/pull/8240))
- **docs(readme):** standardize all README tables to full content width ([#7666](https://github.com/diegosouzapw/OmniRoute/pull/7666))
- **docs:** fix three stale references failing the fabricated-docs gate ([#7728](https://github.com/diegosouzapw/OmniRoute/pull/7728))
- **docs(readme):** unified animated card system — audited v3.8.49 numbers, style contract across all cards, 5 new cards + rebuilt terminal ([#7769](https://github.com/diegosouzapw/OmniRoute/pull/7769))
- **docs(readme):** add Kimi (Moonshot AI) official supporter section ([#7770](https://github.com/diegosouzapw/OmniRoute/pull/7770))
- **docs(getting-started):** reorder Verify It Works before IDE/CLI setup + add examples ([#7790](https://github.com/diegosouzapw/OmniRoute/pull/7790)) — thanks @swingtempo
- **docs(readme):** audit every number against the live code + refresh contributors and acknowledgments ([#7795](https://github.com/diegosouzapw/OmniRoute/pull/7795))
- **docs(readme):** evolve supporter section into sub2api-style Sponsors section ([#7799](https://github.com/diegosouzapw/OmniRoute/pull/7799))
- Add reasoning-based model and effort routing ([#7607](https://github.com/diegosouzapw/OmniRoute/pull/7607)) — thanks @JxnLexn
- **refactor(sse):** extract per-provider token-refresh functions from tokenRefresh.ts ([#7817](https://github.com/diegosouzapw/OmniRoute/pull/7817))
- **deps:** bump the production group with 8 updates ([#7897](https://github.com/diegosouzapw/OmniRoute/pull/7897)) — thanks @dependabot[bot]
- **deps:** bump the development group with 5 updates ([#7898](https://github.com/diegosouzapw/OmniRoute/pull/7898)) — thanks @dependabot[bot]
- **refactor(antigravity):** align official clients and callable catalog ([#8013](https://github.com/diegosouzapw/OmniRoute/pull/8013)) — thanks @backryun
- **refactor(compression):** extract resolveHeadroomDetail to keep dispatchCompression under the complexity gate ([#8058](https://github.com/diegosouzapw/OmniRoute/pull/8058))
- **deps:** bump next from 16.2.10 to 16.2.11 ([#8235](https://github.com/diegosouzapw/OmniRoute/pull/8235)) — thanks @dependabot[bot]
### 🩹 Direct release-branch fixes (no PR — authorized base-red sweep, 2026-07-18)
- **fix(base-red):** full-suite realignment after the 102-PR merge campaign: two real production fixes (legacy `refresh_token` column healed before its index is created; `shouldSkipCloudSyncInitialization` no longer swaps its `(env, argv)` arguments) plus 13 test files, goldens, provider counts, and env docs realigned to the live-validated behavior of the merged PRs.
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.
- **feat(oauth):** Add a one-click browser (PKCE) login for Grok Build (`grok-cli`) ALONGSIDE the existing device-code flow — reusing the same `auth.x.ai` authorize/token endpoints and public client id as the sibling `xai-oauth` provider on its own loopback port — while keeping the pre-existing device-code method and the paste-token/`auth.json` import flow both available; the connect modal lets the user pick "Device Code", "Browser Login", or "JWT Token" ([#7013](https://github.com/diegosouzapw/OmniRoute/issues/7013))
- **feat(shared):** Add canonical numeric coercion helpers (`toNumber`, `toNumberOrNull`, `toNumberArray`) in `src/shared/utils/numeric.ts` with table-driven tests and a lint rule barring new local `toNumber` definitions, then migrate the report/analytics tier (usage analytics route, provider metrics route, usage stats, usage history helpers, call-log formatting, cost-analysis A2A skill) off their local duplicates ([#7879](https://github.com/diegosouzapw/OmniRoute/issues/7879))
- feat(mcp): add a narrow `mcp:connect` API-key scope for the `/api/mcp/` LOCAL_ONLY carve-out (separate from `manage`/`admin`) and populate `authInfo.scopes` from the caller's real per-key scopes over HTTP/SSE so `scopeEnforcement.ts` prefers per-key scopes over the `OMNIROUTE_MCP_SCOPES` env fallback when enforcement is enabled (#7895).
- Teach the model the CCR retrieve protocol (marker → `omniroute_ccr_retrieve`, verbatim 24-char hash, `dedup:ref` look-back) on the first marker, gated on MCP-tool capability (#8033)
- Add a per-model/endpoint compression exclusion filter: name model ids or `provider/model` patterns (`*` wildcard) that must never be compressed, checked before any engine runs so the request passes through byte-identical; configurable from a new Dashboard → Compression → Exclusions tab (#8034)
- **feat(i18n):** zh-CN terminology glossary + consistency gate + 提供商→提供者 normalization — one-shot pass across `src/i18n/messages/zh-CN.json` (679 substitutions) and `bin/cli/locales/zh-CN.json` (54) mirroring #8024's zh-TW pass, a new versioned glossary (`scripts/i18n/glossary/zh-CN.json`) and protected-terms list (`scripts/i18n/glossary/protected-terms.json`), a pure-function consistency check (`scripts/i18n/check-glossary-consistency.mjs`, `npm run i18n:check-glossary`) wired into CI as `i18n-glossary-zhcn`, and zh-CN added to the visual-QA harness default locales (#8038).
- **feat(dashboard):** The Codex quota card no longer shows latent, never-used per-feature rate-limit ceilings (e.g. `GPT-5.3-Codex-Spark`) as permanent 100% rows with a meaningless always-full reset — they now appear only once the feature is actually used. Per-feature window labels track the payload's `limit_name`, and each window is labeled by its real `limit_window_seconds` (so a 7-day `primary_window` shows "Weekly" instead of "Session"). ([#8051](https://github.com/diegosouzapw/OmniRoute/issues/8051))
- 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)
- fix(db): stop closing the single sql.js singleton during getDbInstance()'s probe/reopen sequence, which poisoned every boot with "Database closed" once both sync SQLite drivers were unavailable (#7494)
- 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)
- 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)
- **grok-cli:** OAuth paste-import now requires the full `~/.grok/auth.json` (with `refresh_token`) instead of a bare JWT `key`, preventing connections that can never auto-refresh (#7610).
- **Chat Core**: client aborts that reject the upstream fetch with a raw string reason (e.g. `request_signal_aborted`, `Client disconnected`) are now reported as `499 Request aborted` in the response, request logs, and usage records instead of `FAILED 502 / Bad Gateway`. Follow-up to #7908 for #7907.
- fix(oauth): warn instead of silently opening an unreachable localhost:1455 redirect for Codex/xAI/Grok CLI OAuth when the dashboard is reached from a LAN IP (#8046)
- **fix(sse):** run the prompt-compression pipeline per turn in the Codex Responses WebSocket bridge instead of skipping it entirely — a reused WebSocket connection now re-runs `prepare()` (auth/policy/memory/reasoning-routing/compression) for every logical `response.create` turn, not just the first (#8052)
- **fix(oauth):** Refreshing the token on an email-less OAuth connection (e.g. GitHub Copilot, whose identity lives under `providerSpecificData`) no longer creates a duplicate connection. `persistOAuthConnection` gated its whole dedup behind `if (tokenData.email)`, so the explicit `connectionId` passed on refresh was discarded and a new connection was created every time; the connectionId is now honored regardless of email, and email dedup only applies when a non-empty email is present. ([#8059](https://github.com/diegosouzapw/OmniRoute/issues/8059))
- **fix(providers):** add the missing `poe` REGISTRY entry so built-in Poe model discovery resolves a base URL instead of failing with "No base URL configured for provider" ([#8082](https://github.com/diegosouzapw/OmniRoute/issues/8082))
- 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)
- fix(sse): give keyless Pollinations image requests the same anonymous fingerprint-pool fallback the chat path already uses, instead of a real upstream 401 (#8085)
- fix(cli): surface the real spawn error (err.code/path/syscall/message) in the process supervisor instead of silently swallowing it on a child spawn failure (#8091)
- **fix(antigravity):** preserve request/tool fidelity, make credits modes explicit, and separate runtime endpoints from discovery/bootstrap ([#8098](https://github.com/diegosouzapw/OmniRoute/pull/8098)) — thanks @nguyenha935
- fix(dashboard): clamp provider-cooldown min/max ms to their field bounds before Save so an out-of-range typed value can never reach the resilience settings API (#8107)
- **fix(providers):** the Providers page Learn more button now opens the maintained GitHub documentation instead of the retired `docs.omniroute.io` page ([#8284](https://github.com/diegosouzapw/OmniRoute/pull/8284)) - thanks @KaynXu
- **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
- **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).
- 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)
- 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)
- 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)
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.