fix(server): keep a gateway's Claude reset time on a direct 429#25439

merged2
main
GitHub fork: fix/claude-429-reset-after
opened16d agobyTtyler-b4innova
View on GitHubTwo-way sync enabledPR last imported 9d ago

Problem

When a Claude session uses an API key through a gateway such as CLIProxyAPI, and the gateway answers 429 because all its credentials are cooling down, the "Usage limit reached" banner says "Reset time unavailable; retry manually". This happens even though the error text carries the reset, for example (reset after 1h2m3s).

In API-key mode Claude Code sends no rate_limit_event and no api_error_params.rate_limit_info, even when the response has Anthropic's anthropic-ratelimit-unified-* headers. So the error text is the only place the reset survives. The adapter already parses (reset after …) for the wrapped gateway shape (API Error: 503 [...] [429]: {...} (reset after …)), but not when the 429 arrives directly. That path also starts with an assistant rate_limit frame, which sets a failure hint and skips the gateway check.

Change

providerFailureFromResult now also reads (reset after …) from a 429 result that starts with API Error:, and sets resetAt on the usage_limit failure. The duration parsing that the existing 503 gateway path already used is moved into gatewayCooldownResetAt, which both paths share. The 503 path behaves the same. A 429 without the suffix still gets no reset time, as before.

Scope and approval

A small, focused fix of an existing capability: the adapter already turns a gateway's (reset after …) into the banner's reset time. This makes the same gateway signal work when the 429 is reported directly rather than wrapped in a 503. There's no new setting, workflow, or UI change, and it touches only the Claude adapter's result parsing and its test file.

Verification

  • A new it.effect.each in ClaudeAdapterV2.test.ts ("keeps a gateway's reset on a direct 429") covers three cases: (reset after 1h2m3s), (reset after 4m 6s) (last error: …), and no suffix. It failed on main for the two suffix cases (expected null to equal '…') and passes with the fix. The no-suffix case still expects resetAt: null.
  • vp test run apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts: 238 passed, including the existing wrapped-503 gateway cases.
  • vp run check:changed: format, lint, knip, graphql, typecheck and tests all pass.
  • Runtime transcript: Claude Code 2.1.310 in -p --output-format stream-json mode, pointed at a local upstream that answers 429 the way the gateway now does. It ran with ANTHROPIC_API_KEY and Retry-After: 3723. These are the frames the test feeds the adapter:
1{"type": "assistant", "error": "rate_limit", "api_error_status": 429, "api_error_params": null, "text": "API Error: Request rejected (429) · All credentials for model claude-sonnet-4-5 are cooling down via provider claude (reset after 1h2m3s)"}
2{"type": "result", "subtype": "success", "is_error": true, "terminal_reason": "api_error", "api_error_status": 429, "result": "API Error: Request rejected (429) · All credentials for model claude-sonnet-4-5 are cooling down via provider claude (reset after 1h2m3s)"}

Before the fix, with this output, the thread showed "Reset time unavailable; retry manually".

Not checked: a full T3 client session against a live gateway, and subscription (OAuth) sessions, which already get rate_limit_event and don't take this path.

Written by Claude Opus 5.5 in Claude Code.

Checks

22 successful · 14 skipped
All checks passed
97e43b5

Added 1 commit

  • fix(server): keep a gateway's Claude reset time on a direct 429498d53a
Synced from GitHub

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

ReviewStatusCommitReview trigger
📝 Code Review⚠️ Failed 2026-10-26T10:35:16.689303Z
We couldn’t complete this review. Comment @codex review to try again.
498d53aDraft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Synced from GitHub

Review in Change Stack →

No actionable comments were generated in the recent review.

External image Recent review info
External image Run configuration
External image Commits

Reviewing files that changed from the base of the PR and between 498d53aea5274343c52bf374da1b4c4b64deba22 and 97e43b5568f1d0f5b0eb0a2378363a106f9b0400.

External image Files selected for processing: 2
  • apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts
  • apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts

Limit details: You’ve used all 10 included reviews currently available.

Walkthrough

ClaudeAdapterV2 now parses cooldown text from selected gateway and API-key rate-limit errors. It uses valid cooldowns to set reset times in provider failures. Tests cover two cooldown formats and a missing cooldown.

Changes: Claude cooldown handling

Layer / File(s)Summary
Parse cooldowns into provider failures
apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts, apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts
The adapter uses a shared helper to calculate reset times for gateway 503 rate limits and API-key 429 errors. It leaves the reset time unset when the cooldown is invalid or absent. Tests cover two cooldown formats and a missing cooldown.

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix

Suggested labels: bugfix

Suggested reviewers: juliusmarminge, macroscopeapp

Merge Risk: External image Minimal · up to 97e43

Direct 429 errors can retain gateway reset times. No merge-blocking issue was identified.

Pre-merge checks: Passed 4
Check nameStatusExplanation
Title checkPassedThe title clearly and concisely identifies the main change: preserving a gateway Claude reset time on a direct 429 response.
Description checkPassedThe description includes all required sections. It explains the problem, the implementation, the focused scope, approval rationale, targeted and broader verification, observed results, and remaining u…
Linked Issues checkPassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes checkPassedCheck skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description includes all required sections. It explains the problem, the implementation, the focused scope, approval rationale, targeted and broader verification, observed results, and remaining unverified scenarios.

Finishing Touches
Generate unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Usage-based review receipt

Note

This review was completed with usage-based billing: files reviewed beyond your plan's included limits are billed at $0.25/file. View usage-based billing.

Added 1 commit

  • Merge remote-tracking branch 'github/main' into takeover-2543897e43b5

Note

Written by gpt-6.1-sol on behalf of Maria

merged current main into the existing head without conflicts or source changes. the diff remains two files, +90/-19. blacksmith vp run check:changed github/main passed format, lint, knip, graphql, affected typechecks, and 286 tests, including 285 claude adapter tests. two independent scoped reviews found no actionable issues. live gateway/client verification remains unverified.

Note

Written by gpt-6.1-sol on behalf of Maria

Verified in a real dev environment

partial verification of head 97e43b5568f1d0f5b0eb0a2378363a106f9b0400, with no source changes.

ran vp i and a background vp run dev on github/main (ce9e016406), then stopped that server and ran the exact pr head with isolated state. drove /usr/bin/google-chrome through the repo's playwright dependency. created the project and threads through the web app, pointing at a real git repository. no mocks, fixtures, or demo data.

  • main: real codex gpt-6.1-sol, claude haiku 5.5, and claude sonnet 5.5 turns completed through the local gateway.
  • pr: real codex gpt-6.1-sol and claude haiku 5.5 turns completed. a claude follow-up returned its expected reply. stop showed "run interrupted / stopped by user", and a new message in that thread completed afterward.
  • vp test run apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts on blacksmith: 285 passed, including direct-429 suffix/no-suffix and wrapped-503 coverage. github head checks also passed. these tests do not establish live cooldown behavior.

real claude turns, follow-up, and stop on the pr head

a real claude message completes after stopping the previous turn

server CLI transcript of the completed claude thread

Not verified

the actual fix remains unverified. the real gateway returned successful claude responses, so i could not reproduce main's missing cooldown or observe the pr preserving a direct 429's (reset after ...) in the usage-limit banner. i did not fabricate upstream errors or exhaust credentials to force this condition. wrapped-503 cooldowns, suffix-free live 429s, oauth rate-limit events, automatic retry at reset, remote/tunnel connections, electron, and native mobile clients were not exercised. this is not a claim that the changed behavior works end to end.