Skip to content

fix(sse): stop retry wave on rate-limited 429 and drain 429 once - #13657

Merged
diegosouzapw merged 12 commits into
cheaper-inference:release/v3.8.51from
maxmad64bis:fix/rate-limited-429
Sep 16, 2026
Merged

diegosouzapw merged 12 commits into
cheaper-inference:release/v3.8.51from
maxmad64bis:fix/rate-limited-429

Conversation

@maxmad64bis

@maxmad64bis maxmad64bis commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Summary

A chat request can die with a bare 500 after trying every account one by one. The loop now stops at the first proven rate limit, spares the remaining accounts, and returns a 429 with the relayed Retry-After instead. Bursts and 5xx keep rotating as before (transient 5xx).

Related Issues

Validation

  • Change type: provider
  • Focused tests and category gates from the golden path
  • npm run lint — clean on the touched files; the full run is red on the base (inherited, out of scope)
  • Reconciled with the current active release base; focused checks rerun afterward
  • Production-code changes include a new or updated automated test in this PR

Tests Added Or Updated

  • tests/unit/opencode-rate-limited-classify.test.ts (9 tests green: Retry-After sorts rate-limited, body signals sort rate-limited, anything else sorts burst, 5xx never classified) plus router branch on fixture (classified 429 stops after 1 dispatch, spared accounts counted).
  • Gates rerun on merge-base: typecheck:core clean, targeted eslint clean, file-size OK, new-code complexity green.

Coverage Notes

Health gate green on the touched accounts (no two live ones share a route). Error bodies captured into fixtures for the signal list.

Reviewer Notes

  • Headers first, then bounded body (8 KB), then status; 5xx never body-read. A wrongly stopped burst costs one account, bounded by headers-first plus the body cap; nothing is persisted here.
  • Rollback is one revert with no persisted state.

…fault off)

Gates the classified-429 early stop from cheaper-inference#13657. With the flag off every
opencode 429 keeps rotating to the next account (cheaper-inference#9611).
…real upstream 429

- The cross-account 429 wave is unchanged by default (free tier is per egress
  IP, cheaper-inference#9611); stopping at a classified 429 needs
  OPENCODE_RATE_LIMITED_429_EARLY_STOP.
- When the flag stops the wave, the real upstream 429 is returned as-is:
  status, body, Retry-After and quota headers stay intact, so
  opencode-monthly-quota-resets-in and the quota header/body rules in
  providerErrorRules.ts still match. The synthetic unavailableResponse drain,
  the invented Retry-After: 1 and the x-opencode-retry-state header are gone.
- classifyUpstream429 checks Retry-After first and only then reads a bounded
  8 KiB prefix from a clone, cancelling the clone reader; the original body is
  never consumed. classify429 drops its unused status input.
- The rate-limited account is still cooled down before the stop.
maxmad64bis and others added 7 commits September 15, 2026 20:58
…ailures (cheaper-inference#13615)

Behind the new `OPENCODE_TRANSIENT_FAILOVER_BACKOFF` flag (default off), after two consecutive transient upstream failures the opencode rotation pauses before each later account (1.5s, 3s, 6s, capped at 10s per request) instead of hammering the upstream.

Maintainer rework before merge (kept the idea, no default behavior change):
- The pause honors the client abort signal (no dispatch after a disconnect), the failed attempt's body is cancelled before sleeping, `transientRetryDelayMs` now uses its arguments, and the sleep is injectable so the tests run without real timers.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
…o clear auth failure (cheaper-inference#13609)

Behind the new `MISTRAL_AMBIGUOUS_401_SOFT_LOCKOUT` flag (default off), a bare Mistral 401 (`{"detail":"Unauthorized"}`, identical for a revoked key and an exhausted quota) gets a retryable cooldown instead of parking the connection as `expired`; after three soft strikes within an hour the next bare 401 parks it, so revocation still converges.

Maintainer rework before merge (kept the idea, no default behavior change):
- The predicate is shared with the connection-test module instead of duplicated; the squeezed 139-char line that dodged the file-size gate is formatted normally and the growth is rebaselined honestly with an annotation.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
# Conflicts:
#	docs/reference/FEATURE_FLAGS.md
#	open-sse/executors/opencode.ts
#	src/shared/constants/featureFlagDefinitions.ts
#	src/shared/utils/featureFlags.ts
#	tests/unit/feature-flags-settings.test.ts
#	tests/unit/server-owned-tool-loop-flag.test.ts
…d ERROR_TYPE_CONTRACT import left by the batch merges (cheaper-inference#13816)

Merged with admin on local + CI evidence: `tests/unit/i18n-catalogs-no-duplicate-keys.test.ts` red on the tip (59 catalogs) → `pass 3 / fail 0` here; **API Route Typecheck passes on this PR** (it fails on every PR based on the current tip because of the duplicated `ERROR_TYPE_CONTRACT` import this removes); CodeQL, semgrep, Vitest fast-path, Docs gates, Change Classification pass. The remaining red checks (Fast Quality Gates, Merge integrity, Unit Tests fast-path 1/2/4) are the same inherited tip reds every PR on release/v3.8.51 shows right now — cheaper-inference#13747 sweeps them. Both removed lines were byte-identical duplicates; nothing parsed or typed changes.
@diegosouzapw
diegosouzapw merged commit 997cd4d into cheaper-inference:release/v3.8.51 Sep 16, 2026
3 of 7 checks passed
@maxmad64bis
maxmad64bis deleted the fix/rate-limited-429 branch September 16, 2026 18:14
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…aper-inference#13657)

The opencode executor classifies rate-limited 429 bodies (`classify429`, with real tests) and, when a whole account wave is exhausted, returns the last real upstream 429 — status, body, `Retry-After` and quota headers intact — so the provider error rules (monthly-quota cooldown) keep working.

Maintainer rework before merge (kept the idea, no default behavior change):
- The original stopped the cross-account wave at the first classified 429 and replaced the response with a synthetic one that dropped the body and headers; stopping early is now opt-in behind `OPENCODE_RATE_LIMITED_429_EARLY_STOP` (default off), the rate-limited account is still cooled down, the body is read as a bounded 8 KiB prefix from a clone and the original is never consumed, and the unused `status` input is gone.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 d61b804: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
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