The 3 tests/boundary/*.live.test.ts files merged via #6786 hardcoded a real
Bearer API key, an auth_token JWT cookie, and a live instance URL. Replaced with
env reads (OMNIROUTE_TEST_BASE/BEARER/COOKIE), preserving the RUN_BOUNDARY_LIVE gate.
The leaked key/cookie must still be revoked/rotated on the affected instance and
purged from history separately (operator action).
extractUsageFromResponse() and normalizeUsage() used Number(x)
coercion for cost_in_usd_ticks, which silently turned null/""
into 0 -- accepted downstream as a valid $0 exact cost instead of
falling back to the token-based estimate. Both call sites now
require typeof === "number" && Number.isFinite && >= 0.
Rebased onto current release/v3.8.47 tip (already carries #6711)
and trimmed to just the incremental validation fix + 2 regression
tests, replacing the stale-base diff that re-added the whole
already-merged feature.
Co-authored-by: KooshaPari <koosha@phenotype.io>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(providers): scope nvidia NIM 404s to the single failing model (#6773)
The nvidia registry entry multiplexes 17 models from 9 different upstream
vendors (z-ai/, minimaxai/, deepseek-ai/, qwen/, mistralai/, stepfun-ai/,
moonshotai/, openai/, nvidia/) behind one connection, but was missing
passthroughModels: true — unlike 34 other multi-model registries
(modelscope, synthetic, kilo-gateway, etc). Without it, hasPerModelQuota
returns false for nvidia, so a 404 on a single stale/renamed model falls
through checkFallbackError's generic catch-all as a connection-wide
cooldown instead of being scoped to just that model, poisoning all 17
nvidia models for the cooldown window.
Add passthroughModels: true to the nvidia registry entry so 404/429s on
one model lock out only that model.
Regression test: tests/unit/nvidia-passthrough-models-6773.test.ts
* fix(quality): register nvidia passthrough test in stryker tap.testFiles
check-mutation-test-coverage.mjs --strict flagged
tests/unit/nvidia-passthrough-models-6773.test.ts as an unregistered
covering test for accountFallback.ts (Fast Quality Gates).
Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>
---------
Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>
* fix(cli): ship head-response-guard.cjs in the standalone bundle
server-ws.mjs imports ./head-response-guard.cjs, but assembleStandalone had
no EXTRA_MODULE_ENTRIES entry for it, so every build:release bundle crashed
at boot with ERR_MODULE_NOT_FOUND (found deploying d1d75fdbf to the VPS on
2026-07-11). Adds the missing entry plus a regression test that derives the
required sidecar list from server-ws.mjs's own relative imports, guarding
the whole class of missing-sidecar bugs.
* docs(changelog): fragment for #6908
archiveLegacyRequestLogs() swept the entire DATA_DIR/logs directory as a
single "legacy" target and recursively deleted it after zipping. Since
PR #6234 moved the default app-log path to DATA_DIR/logs/application,
the migration was deleting the live file logger's own directory on
every boot until its marker file existed (#6799).
Separately, yazl's addFile() does an internal stat-then-stream read; if
a target file grows between those two steps (e.g. an actively-written
log), yazl emits "error" directly on the ZipFile instance. That event
had no listener, so Node re-threw it as an uncaughtException that
crashed the whole process at startup (#6401) — misdiagnosed upstream as
Turbopack/Windows chunk corruption because the stack trace pointed into
a bundled chunk.
Fix:
- listArchiveTargets() now enumerates DATA_DIR/logs entries individually
and skips the live app-logger directory (resolved via
logEnv.getAppLogFilePath()), so the shared parent directory is never
deleted wholesale.
- createLegacyArchive() wires a zipFile.on("error", ...) handler so a
stat/stream race rejects the promise (caught by the existing
try/catch) instead of escaping as an uncaughtException.
Regression test: tests/unit/usage-migrations-legacy-archive-safety.test.ts
(RED on unfixed code, GREEN after fix). Updated
tests/unit/request-log-migration.test.ts to the corrected contract —
DATA_DIR/logs itself now survives the archive sweep.
Gates run clean: file-size, complexity, cognitive-complexity,
changelog-integrity, typecheck:core, eslint (suppressions), and the
existing usage-migrations/request-log-migration unit suites.
* fix(sse): de-flake timing-sensitive combo cooldown/breaker tests (#6803)
Extracts 3 wall-clock-sensitive assertions (combo-quota-share cooldown
ceiling x2, circuit-breaker HALF_OPEN race) into tests/unit/serial/
(--test-concurrency=1, the repo's established remedy for this class of
test) and widens their margins, since a starved CI-runner event loop can
blow even a serialized test's timing window. Also adds an explicit 30s
vitest timeout to the MCP audit shutdown test, which had no override and
inherited vitest's 5000ms default.
Regression proof: reproduced RED locally under real devbox CPU
contention (2644ms/1796ms elapsed vs the old 1500ms ceiling, exactly the
reported failure mode); confirmed GREEN after the fix under the same
contention.
* fix(quality): register new serial timing tests + prune stale any-suppression count
- stryker.conf.json: add tests/unit/serial/combo-quota-share-cooldown-wait-timing.test.ts
and tests/unit/serial/combo-strategy-fallbacks-half-open-timing.test.ts to
tap.testFiles so their mutant kills count for accountFallback.ts and
circuitBreaker.ts (PR #6897 added these files but didn't register them).
- eslint-suppressions.json: combo-strategy-fallbacks.test.ts's no-explicit-any
suppression count was stale (35) after this PR trimmed 2 any-usages out of
the file when extracting the half-open timing test; corrected to 33.
Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>
---------
Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>
waitForServer() polled /api/monitoring/health but fell back to declaring
the server ready once the port had merely accepted TCP connections for
>= 3s, even if no HTTP response was ever received. On CPU-bound warmup
(e.g. small VPS running Next.js standalone), the OS-level listener
accepts TCP almost immediately while the request pipeline is still
compiling, so the fallback fired within ~3-7s and the CLI printed
'OmniRoute is running!' 30-60s before any route actually answered.
Classify each health poll into ready / fast-reject / hanging /
not-listening: only a fast HTTP rejection (fetch error that is not a
timeout, e.g. ECONNRESET before the route mounts) grants the original
#2460 Windows-cold-start grace window. A request that times out with
zero response (the reported #6800 symptom) resets the grace window
instead of accumulating toward it.
Regression tests: tests/unit/waitForServer-tcp-fallback-6800.test.mjs
(new RED-then-GREEN probe from the bug analysis) and
tests/unit/cli-waitForServer.test.mjs (existing suite realigned to the
corrected contract, plus a new case for the hanging-socket scenario).
* fix(providers): give v0-vercel-web its own alias so credentials are detected (#6343)
* test(6343): type casts to satisfy no-explicit-any gate
* test(6343): register v0-web + cliproxyapi tests in stryker tap.testFiles
The two unit tests added on this branch cover mutated modules
(src/sse/services/auth.ts, comboContextCache.ts) but were missing from
stryker.conf.json tap.testFiles, tripping check:mutation-test-coverage --strict.
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
* fix(ci): exclude check-test-masking.test.ts fixtures from self-referential tautology gate (#6634)
* fix(ci): extend test-masking self-fixture exclusion to sibling gate regression files (#6634)
The #6634 fix added isSelfTestFixtureFile()/scanBareTautologies() exclusions
that only matched check-test-masking.test.ts exactly. Its own new regression
file check-test-masking-selfref-6634.test.ts also embeds tautology-pattern
literals as fixtures/documentation, so the absolute-floor scanBareTautologies
gate self-tripped a HARD failure on the PR's own file. Generalize the
exclusion to the whole check-test-masking* self-test family and lock it with
two regression tests.
* fix(ci): publish electron-updater latest.yml manifests in release assets (#6766)
* fix(ci): register new mutation-covering test + realign stale codex-cli version fixture
- stryker.conf.json: add tests/unit/cliproxyapi-model-mapping-dispatch.test.ts to
tap.testFiles so it counts toward mutation coverage for the newly-added
comboContextCache.ts coverage (check:mutation-test-coverage --strict was failing).
- tests/unit/provider-models-route-codex.test.ts: DEFAULT_CODEX_CLIENT_VERSION was
bumped to 0.144.0 on release/v3.8.47 after this test's fixtures were written;
realign the hardcoded 0.142.0 expectations to the current constant.
getExplicitModelOutputCap() (the clamp ceiling used by
resolveReasoningBufferedMaxTokens) only ever read the unvalidated synced
limit_output / registry / static-spec chain — it ignored the operator-settable
max_token capability override that getResolvedModelCapabilities() already
consulted. When a provider's synced catalog row reports a wrong limit_output
(e.g. ollama-cloud/deepseek-v4-flash: limit_output=1048576, same as
limit_context, while the real upstream cap is 65536), the reasoning-buffer
clamp trusted the bad number and inflated max_tokens 64000 -> 96000, which
upstream rejected with "exceeds model's maximum output tokens (65536)".
The override table (model_capability_overrides, "max_token" key,
/api/model-capability-overrides) is the existing, already-shipped remediation
path for exactly this class of bad catalog data, but reasoningTokenBuffer.ts
had no way to benefit from it. Extracted the override lookup into a shared
getMaxTokenCapabilityOverride() helper and made getExplicitModelOutputCap()
consult it first, so both read paths now agree.
TOTAL_MCP_TOOL_COUNT in open-sse/mcp-server/server.ts summed collection sizes
additively, double-counting tools registered in more than one collection.
The agent-skills trio (omniroute_agent_skills_list/get/coverage) is
intentionally defined in both MCP_TOOLS (schemas/tools.ts) and
agentSkillTools (tools/agentSkillTools.ts), inflating the reported count
from 96 unique tools to 99.
Replace the additive sum with countUniqueMcpTools() (new
open-sse/mcp-server/toolCount.ts), which unions all collection tool
names into a Set before counting, so any future overlap self-corrects
instead of double-counting.
Regression test: tests/unit/mcp-tool-count-dedup-6854.test.ts
Two related root causes in the stacked compression pipeline:
- #6479/#6491: a dispatched step whose engine legitimately finds nothing
eligible (session-dedup with no repeated blocks, ccr below its min-chars
threshold) returns `{ stats: null }`. `mergeStackStep()` silently dropped
that step from `engineBreakdown` with zero trace — no warning, no error.
Now records a `"<engine>: skipped (no eligible content)"` validation
warning for any null-stats step, covering every engine that follows this
convention (session-dedup, ccr, headroom, relevance, llm, llmlingua,
ionizer, readLifecycle), not just the two reported.
- #6480: `finalizeStackedResult` ran the aggregate `guardPipelineInflation`
check unconditionally, even when the loop-level `compressed` flag stayed
false (no step ever advanced `currentBody`). Since tokens are trivially
equal when nothing ran, the guard mislabeled a genuine no-op as
`fallbackApplied: true` with a misleading "reverted to original" warning.
Extracted the guard into `applyStackedInflationGuard()` in
`pipelineGuards.ts` (keeps `strategySelector.ts` under its frozen line
budget) and gated it on `compressed === true`.
Also fixes `compression-pipeline-inflation-guard.test.ts`'s wire test,
which passed a bare engine-id string to the pipeline; `normalizePipelineStep()`
only recognizes a fixed set of built-in string aliases and silently
downgrades any other string to `{ engine: "caveman" }`, so the test's
custom inflating engine was never actually exercised. Passing a step object
restores the test's original intent.
New regression tests: tests/unit/compression/repro-6479-6491-null-stats-silent-drop.test.ts,
tests/unit/compression/repro-6480-noop-guard-misfire.test.ts.
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 second, unprefixed omnirouteProviderId field and thread it through the
four dynamic-hook call sites that emit server-facing identifiers, while
leaving the OC-gate-prefixed providerId in place for AuthHook.provider,
provider registration (hook.id), and the static-catalog path (which OC
strips before dispatch, per the existing static-block comments).
- 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.
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.
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).
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.
* 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)
* 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(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)
* 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>
* 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.
* 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)
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).
* 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>
* 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>
- 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
* 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>
* 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(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(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>
* 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(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)
* 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)
* 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(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>