Repository navigation
fix(resilience): park Gemini PerDay quota violations until the daily reset - #16705
Merged
diegosouzapw merged 3 commits intoOct 15, 2026
Merged
Conversation
…reset A Gemini free-tier 429 whose google.rpc.QuotaFailure names a per-day quotaId (e.g. GenerateRequestsPerDayPerProjectPerModel-FreeTier) still carries a ~20s RetryInfo. Honoring only that hint re-burned the exhausted key on every rotation until midnight Pacific. - retryAfterJson: hasGoogleDailyQuotaViolation / googleDailyQuotaIds read the QuotaFailure details (strict @type + quotaId match, bounded raw-text fallback). - googleDailyQuota: cooldown until the operator's node daily-reset clock, else the next midnight America/Los_Angeles; appendGoogleDailyQuotaViolation keeps the signal next to the prose message the classifiers read. - checkFallbackError returns quota_exhausted + dailyQuotaExhausted with quotaResetHintMs, so per-model providers (gemini) lock only the model; parseRetryFromErrorText returns the same daily window. - chatCore pipeline (non-streaming) and streaming leg carry the compact QuotaFailure line into the classification text; client bodies keep the first line only. Co-authored-by: Scott Christian <scottchristian.tas@gmail.com> Inspired-by: decolua/9router#4348
Contributor
CI Coverage Report
Coverage artifact was not available for this run. |
diegosouzapw
added a commit
that referenced
this pull request
Oct 15, 2026
## Summary - Re-measure the five files this owner merge-batch pushed over the frozen ceilings, on release/v3.8.52 at d50cd80. No slack. - `open-sse/executors/base.ts` 1743→1745 (#16621), `open-sse/handlers/chatCore/streamingResponse.ts` 1308→1309 (#16705), `open-sse/handlers/chatCore.ts` 3938→3943 (#16720), `open-sse/translator/response/openai-responses.ts` 1547→1590 (#16645), `tests/unit/provider-validation-specialty.test.ts` 2912→2965 (#16625). - `node scripts/check/check-file-size.mjs` exits 0 on this branch. README drift (#16132) and the inherited doc-source drift were not stamped.⚠️ base-red inherited: #15306 Thanks @diegosouzapw.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
google.rpc.QuotaFailurenames a per-dayquotaId(e.g.GenerateRequestsPerDayPerProjectPerModel-FreeTier) now locks that model until the daily reset. Before, the ~20sRetryInfoattached to the same body was the only signal used, so an exhausted free-tier key went back into rotation every ~20s until midnight Pacific.RetryInfocooldown (fix(resilience): Model Lockouts ignore configured cap and upstream retry hint (freeze until midnight) #7940 behavior unchanged).Attribution
Thanks to @scottchristian for the original implementation.
Changes
open-sse/services/retryAfterJson.ts:googleDailyQuotaIds()/hasGoogleDailyQuotaViolation()readerror.details[]with a strictgoogle.rpc.QuotaFailure@typeand match/perday/ionquotaIdonly, never on free text. A bounded raw-text fallback (ReDoS-safe) handles a prose message that has the detail appended.open-sse/services/googleDailyQuota.ts(new):googleDailyQuotaCooldownMs()uses the operator's node daily-reset clock when one is configured. Otherwise it waits until the next midnightAmerica/Los_Angeles, Google's documented reset for Gemini's requests-per-day quota. The result is capped at 25h.appendGoogleDailyQuotaViolation()adds a compact QuotaFailure line, holding the PerDay quota ids only, to the classification text.open-sse/services/accountFallback.ts:checkFallbackErrorreturnsquota_exhausted+dailyQuotaExhausted+quotaResetHintMsfor these 429s. The check runs before the Gemini metric and credits checks and is keyed on the body shape, so it covers gemini, vertex and AI-Studio-style connections. For per-model providers,markAccountUnavailableturnsquotaResetHintMsinto a model lockout.parseRetryFromErrorTextreturns the same daily window instead of 26s.open-sse/handlers/chatCore/providerExecutionPipeline.ts(non-streaming) andopen-sse/handlers/chatCore/streamingResponse.ts(streaming): classification only ever sawerror.message. For Gemini that message reads the same for per-minute and per-day exhaustion. Both paths now carry the compact QuotaFailure line in the classification text. Client-facing error bodies keep only the first line, so their content does not change. I checked this withcreateErrorResult.markAccountUnavailablepath, so the operator's model-lockoutmaxCooldownMs(30 min by default) still bounds it. That is the same policy the existing daily-quota (isDailyQuotaExhausted) lockouts follow. The connection-levelcooldownMs/quotaResetHintMscarry the full daily window.rateLimitManageris left as is, because a multi-hour reservoir would queue requests instead of failing over.Test plan
node --import tsx/esm --test tests/unit/gemini-daily-quota-violation.test.ts(new, 11 tests): before the wiring, 4 behavior tests failed (parseRetryFromErrorTextdaily window,checkFallbackErrordaily park, prose+append path, model-only lockout viamarkAccountUnavailable). The streaming source-wiring assertion and the new pipeline test also failed with the call sites reverted. After the change:ℹ tests 11 ℹ pass 11 ℹ fail 0.gemini-daily-quota-violation,provider-execution-pipeline,bug-7940-gemini-retrydelay,gemini-429-retry-hint-skips-intra-retry,account-fallback-service,account-fallback-retry-after-json→ℹ tests 153 ℹ pass 153 ℹ fail 0.accountFallback|retryAfterJson|providerExecutionPipeline|streamingResponse|parseRetryFromErrorText|checkFallbackError|dailyQuotaReset) →ℹ tests 1865 ℹ pass 1861 ℹ fail 4. Two of the 4 pass in isolation (load-sensitive). The other two (copilot-learned-catalog-15634"a listed, supported Copilot model still reaches the HTTP upstream" andprovider-request-failure-pipeline"network failure persisted call log…") fail identically with this PR's source changes reverted, so they come from the base.npm run typecheck:core→ exit 0.npx eslint --suppressions-location config/quality/eslint-suppressions.json <changed files>→ clean;npx prettier --check <changed files>→ clean.node scripts/check/check-pr-test-policy.mjs(PR mode) → PASS;GITHUB_BASE_REF=release/v3.8.52 npm run check:test-masking→ OK (no weakening).npm run check:file-size→ no violation in touched files.accountFallback.tsis at 2487 against its 2504 ceiling andstreamingResponse.tsat 1308 against 1308. The only reported violations are two untouched test files (base drift).