Survive a rate limit instead of losing the task to it - #92
Merged
Conversation
A dispatcher often knows who should look at a fix and had no way to say so. The integration-failure triage is the case in point: when CI's bisect pins a regression to a commit it knows that commit's author, but the fix PR opened with no reviewer request, so the one person who knows what the change was meant to do had to notice it by chance. Accept an optional `reviewers` list of GitHub logins on POST /tasks and request their review after the PR goes draft->ready. serge stays generic: it forwards the request and holds no opinion about who is relevant. Details that matter: - Requested AFTER the draft->ready transition, so it ADDS to whatever the repo's own routing (transformers' assign-reviewers.yml) does rather than racing it. - New PRs only. An existing_pr follow-up pushes onto a branch whose review is already requested, and re-requesting resets a review the author may already have given. - Fail-soft: GitHub 422s the whole call for reasons that are none of the PR's business (the login is not a collaborator, it is the PR's own author, it no longer exists). The PR is published by then, so request_reviewers logs and swallows everything, including transport errors, and returns the logins it actually requested. - Malformed entries are dropped rather than raising, same contract as test_links: a review request is a courtesy on top of the fix and must never cost a PR. Logins are validated against GitHub's charset, which also drops bots (`dependabot[bot]`) — a bot cannot review, and one bad login 422s the whole request, taking the valid ones with it. Deduped case-insensitively and capped at 10 (GitHub's own limit is 15). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
One 429 ended the `deepseek_vl` task on 2026-08-18 after it had already spent 1.13M input tokens (transformers#48050). Two reasons, both fixed here. A 429 shared the 3-attempt budget with 5xx and dropped connections, so three quick tries — about six seconds of patience — threw away half an hour of finished work. But the two failures are not alike: a server error may never clear, while a rate limit clears BY WAITING. Rate limits now get their own, larger budget; a persistent 5xx still fails fast at 3. Retrying the rejected call was also not enough on its own: the loop that tripped the limit is about to issue the same burst of tool turns. So the client now paces itself, and takes the numbers from the server rather than inventing them: - the retry waits what `Retry-After` says, else what the `x-ratelimit-reset` family says, and only falls back to exponential backoff when the endpoint says nothing; - the same value sets the spacing for the calls that FOLLOW; - on a successful response carrying `x-ratelimit-remaining-requests`, the spacing spreads the remaining allowance over the window it resets in — which keeps the loop under the limit instead of discovering it by being refused. Only headroom the server reports as tight (<10 left) slows anything down. Spacing decays on success, so a loop that hit one transient 429 returns to full speed rather than limping. An endpoint that never rate-limits us is never paced, and every wait stays bounded by the existing per-wait ceiling — a server is allowed to be wrong or hostile. Reset values are parsed in the forms providers actually send: plain seconds, `6m0s`/`300ms` durations, and absolute epochs (a deadline, not a 1.7-billion-second wait). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Why
One 429 ended the
deepseek_vltask on 2026-08-18 after it had already spent 1.13M input tokens (transformers#48050):Two causes, both fixed here.
A 429 shared the error budget.
attempts = 3covered 5xx, dropped connections and rate limits, with 2s/4s backoff — about six seconds of patience before throwing away half an hour of finished work. But they are not alike: a server error may never clear, while a rate limit clears by waiting. Rate limits now get their own, larger budget; a persistent 5xx still fails fast at 3.Retrying the rejected call isn't enough. The loop that tripped the limit is about to issue the same burst of tool turns, so the next call gets refused too. The client now paces itself.
The numbers come from the server, not from me
Retry-Aftersays; failing that, what thex-ratelimit-resetfamily says; exponential backoff is only the last resort for an endpoint that says nothing;x-ratelimit-remaining-requests, the spacing spreads the remaining allowance over the window it resets in — so the loop stays under the limit instead of discovering it by being refused. Only headroom the server reports as tight (<10 requests left) slows anything down.Reset values are parsed in the forms providers actually send: plain seconds,
6m0s/300msdurations, and absolute epochs (a deadline, not a 1.7-billion-second wait).Guard rails
675 tests pass (11 new),
make formatclean.🤖 Generated with Claude Code