Skip to content

feat: add InsufficientQuotaError for insufficient_quota 429 responses - #3507

Open
pctablet505 wants to merge 4 commits into
openai:mainfrom
pctablet505:fix-1671-insufficient-quota-exception
Open

feat: add InsufficientQuotaError for insufficient_quota 429 responses#3507
pctablet505 wants to merge 4 commits into
openai:mainfrom
pctablet505:fix-1671-insufficient-quota-exception

Conversation

@pctablet505

Copy link
Copy Markdown

Fixes #1671

Adds InsufficientQuotaError, a subclass of RateLimitError, and maps 429 responses with code="insufficient_quota" to it so callers can distinguish quota exhaustion from retryable rate limits.

Adds InsufficientQuotaError as a subclass of RateLimitError and maps 429
responses with code='insufficient_quota' to it so callers can distinguish
quota exhaustion from retryable rate limits.

Fixes openai#1671
@pctablet505
pctablet505 marked this pull request as ready for review July 17, 2026 12:48
@pctablet505
pctablet505 requested a review from a team as a code owner July 17, 2026 12:48

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e77b297e8f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/openai/_client.py
Comment on lines 693 to +695
if response.status_code == 429:
if is_mapping(data) and data.get("code") == "insufficient_quota":
return _exceptions.InsufficientQuotaError(err_msg, response=response, body=data)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Skip retries for insufficient_quota responses

With the default max_retries, _base_client calls _should_retry() and retries every 429 before _make_status_error() is reached, so this new branch only runs after the retry budget is exhausted; the async path has the same flow. For deterministic code="insufficient_quota" responses this still sends multiple doomed requests and delays surfacing the non-retryable quota error, so the quota code needs to be classified before retrying or handled in _should_retry.

Useful? React with 👍 / 👎.

_should_retry treated every 429 as retryable, so a deterministic
insufficient_quota error still burned the whole retry budget before
_make_status_error got a chance to raise InsufficientQuotaError,
delaying the new error type behind unnecessary retries and backoff
sleeps. Check the error code in _should_retry and skip retrying when
it's insufficient_quota, mirroring the extraction already done in
_make_status_error.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 94462847c6

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

# quota; retrying will not help, so don't burn the retry budget
# on a request that is guaranteed to fail again.
try:
body = response.json()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid retrying streamed quota errors

Fresh evidence after the retry fix: for stream=True/.with_streaming_response calls, _base_client.request() sends the request with streaming enabled, so the 429 body has not been read when this line calls response.json(); httpx raises ResponseNotRead, the broad except turns the body into None, and the code falls through to retry. Streaming chat/responses requests that receive code="insufficient_quota" will therefore still burn max_retries before raising, unlike the non-streaming path covered by the new tests.

Useful? React with 👍 / 👎.

@rkfshakti rkfshakti left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good addition. The insufficient_quota error code is a well-known OpenAI API response, and treating it as non-retryable is the correct behavior — retrying a quota-exceeded request just burns retry budget and delays the user getting the real error.

The implementation is clean:

  • InsufficientQuotaError extends RateLimitError (same HTTP status, different semantics) — callers catching RateLimitError will still catch this, which is backward compatible.
  • The retry guard in _should_retry parses the response body only when status is 429, so there's no performance overhead on normal responses.
  • Both sync and async _make_status_error paths are covered.

One minor thing: the try/except Exception around response.json() could be narrowed to json.JSONDecodeError to avoid swallowing unrelated errors. But since this is in a retry-decision path where the consequence of a false negative is just an extra retry, the broad catch is acceptable.

LGTM.

@jbeckwith-oai jbeckwith-oai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two blockers:

  1. The non-retry behavior does not work for streamed responses. _should_retry() calls response.json() before the error response has been consumed; for stream=True and .with_streaming_response, httpx raises ResponseNotRead, the broad except turns that into body = None, and the 429 falls through to the normal retry path. I reproduced this with a streaming 429 body and max_retries=3: it made 4 requests before finally raising InsufficientQuotaError. Please make body-based retry classification work after the error body is available in both sync and async paths, and add streaming regressions for both APIs.

  2. The repository lint gate is currently red. ./scripts/lint reports unsorted import blocks in src/openai/__init__.py and tests/test_client.py; ruff format --check also reports that tests/test_client.py needs formatting. Please run the repo formatter/linter and commit the result.

_should_retry() calls response.json() to check for insufficient_quota,
but for stream=True requests and .with_streaming_response the body is
not read automatically. httpx raises ResponseNotRead in that case,
which the broad except in _should_retry swallowed, silently falling
back to the normal retry path and burning the whole retry budget on a
429 that could never succeed.

Read the response body in both the sync and async request loops before
calling _should_retry, so the insufficient_quota check works the same
way for streaming and non-streaming responses.

Also run the repo's ruff import sort and formatter to fix the unsorted
import blocks in __init__.py and test_client.py.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8d683d77fb

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

# `stream=True` requests the body is not read automatically and accessing
# it before this point raises `httpx.ResponseNotRead`.
if not err.response.is_closed:
err.response.read()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve retries when streamed error bodies fail

For stream=True/.with_streaming_response calls, a retriable 429 that is not insufficient_quota or any 5xx can have its status headers available while the error body stalls or fails; this unconditional read() then raises (for example httpx.ReadTimeout) before _should_retry() runs, so the default retry path is skipped and a raw transport error leaks. Since only the quota classification needs the body, gate or catch this read so other retriable streamed status errors can still retry based on status/headers; the async path has the same issue.

Useful? React with 👍 / 👎.

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.

insufficient_quota in _exceptions.py

3 participants