Repository navigation
fix(openai-utils): fail fast on spent-quota 429 and invalid-key 401/403 - #3571
Merged
Merged
Conversation
A 429 whose body is a spent key (budget_exceeded, insufficient_quota) was retried like a transient rate limit, then every later judge call hit the same dead endpoint. Trip the shared OpenAI client on that body, and on 401/403, so subsequent requests skip the wire. Co-Authored-By: Claude Code <noreply@anthropic.com> Signed-off-by: FrontierJam Agent <frontierjam@nvidia.com> Signed-off-by: Grzegorz Chlebus <gchlebus@nvidia.com>
β¦ResponseError (F1 F2 F5 F6 F7 F8 F3) Keep the fail-fast latch, but only on parsed error.code/type budget_exceeded or insufficient_quota, and on 401/403 bodies that name an invalid key. Generic per-minute quota 429s and other auth denials stay retryable or returned. PermanentEndpointError is now a ClientResponseError so judge callers still see .status. Trip details are stored as a tuple and each raise builds a new exception; in-flight retries re-check before the next wire call. Co-Authored-By: Claude Code <noreply@anthropic.com> Signed-off-by: FrontierJam Agent <frontierjam@nvidia.com> Signed-off-by: Grzegorz Chlebus <gchlebus@nvidia.com>
Contributor
|
πΏ Preview your docs: https://nvidia-preview-gchlebus-judge-quota-fail-fast.docs.buildwithfern.com/nemo/gym Here are the markdown pages you've updated: |
Contributor
Author
|
/ok to test |
@gchlebus, there was an error processing your request: See the following link for more information: https://docs.gha-runners.nvidia.com/cpr/e/1/ |
Contributor
Author
|
/ok to test 590cd97 |
bxyu-nvidia
approved these changes
Sep 21, 2026
bxyu-nvidia
enabled auto-merge
September 21, 2026 17:55
Contributor
|
π Merge queue validation started! You can track the progress here: https://github.com/NVIDIA-NeMo/Gym/actions/runs/35636030089 |
7 tasks done
8 tasks
This branch was previously deployed
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.
What does this PR do?
NeMoGymAsyncOpenAI._request_with_retrytreated every 429 as a transient rate limit. When the upstream key was actually spent (budget_exceeded/insufficient_quota), each request burned all its retries against a dead endpoint, and every later judge or model call on the same client did the same. In long eval jobs this meant hours of rollouts silently scored against a key that would never answer.This PR adds a per-client fail-fast latch to the shared OpenAI client:
error.code/error.typeisbudget_exceededorinsufficient_quotatrips the client. Generic "quota exceeded" wording (used by per-minute limits that recover) still retries as before.invalid_api_key,invalid_api_token,authentication_error, or "invalid/incorrect api key" text) also trips the client. Other 401/403 responses are returned once, unchanged from today._request/_request_with_retrycall on that client raisesPermanentEndpointErrorwithout touching the wire. In-flight retry loops re-check the latch before their next attempt, so sibling tasks stop as soon as one trips.PermanentEndpointErrorsubclassesaiohttp.ClientResponseError, so existingexcept ClientResponseErrorhandlers (for example the judge paths that read.status) keep working. Each raise builds a fresh exception instead of re-raising a cached one, so rejected requests are not retained.server_utils.raise_for_statusaccepts pre-readcontent, because the retry loop now reads the body before deciding whether to retry. This lets the final attempt raise with the real error body instead of an already-consumed stream.max_http_attemptsrow in the OpenAI model-server docs now describes the retryable set and the fail-fast cases accurately.The latch is per
NeMoGymAsyncOpenAIinstance. Session pinning / failover across multiple model-server instances is out of scope here.No GitHub issue: this came out of an internal eval run where a spent inference key was retried for the remainder of the job. Reviewed and approved internally before being moved here.
Validation
Run against the final diff in this branch:
New tests cover: the quota and auth body classifiers (parametrized over real OpenAI / inference-gateway error shapes), fail-fast on spent-key 429 and invalid-key 401/403, generic-quota and transient 429s still retrying, exhausted transient 429 raising without tripping, auth errors without a key marker being returned once, requests skipping the wire after a trip, an in-flight retry stopping after a sibling trips,
create_chat_completionsurfacing the permanent error's.status, andraise_for_statusaccepting prefetched content.Rollouts
N/A. This changes HTTP retry behavior in the shared client, not environment or agent logic. The behavior is exercised end to end in unit tests with mocked upstream responses. Not exercised against a live spent key.
Compatibility and benchmark impact
max_http_attemptssemantics are unchanged for transient errors.PermanentEndpointError(aClientResponseErrorwith the original.status) immediately instead of retrying. Callers that previously saw the 429/401/403 after exhausting retries now see it on the first request and without the retry delay.Checklist
pre-commit run --all-files) (so CI lint/format checks pass).git commit -s) (so the DCO check passes).π€ Generated with Claude Code