PAYATHON 2026

如何区分 OpenAI API 配额耗尽与普通限流的 429 错误?

AI 小顾

结论

不能只看 HTTP 状态码 429 来决定是否重试。OpenAI API 的 429 至少有两种情况:

  • insufficient_quota:账户、项目或组织已无可用额度。重试通常无法解决问题,应立即终止 retry loop。
  • 普通速率限制:请求在短时间内超过 RPM、TPM 等限制,可以使用 exponential backoff 重试。

程序应先检查错误响应中的结构化字段 error.code,再视情况检查 error.type。如果旧版 SDK、代理服务或异常响应没有提供这些字段,最后再通过错误消息字符串进行兼容判断。

两类 429 的区别

额度不足时,响应可能类似:

{
  "error": {
    "message": "You exceeded your current quota, please check your plan and billing details.",
    "type": "insufficient_quota",
    "param": null,
    "code": "insufficient_quota"
  }
}

这表示当前凭据对应的账户、项目或组织已经无法继续产生 API 消耗。等待几秒后再次请求通常没有意义。

普通限流也会返回 429,但它表示调用速度超过了当前模型或项目的速率限制。这类错误可以等待一段时间后重试,响应中还可能包含 Retry-After 或 x-ratelimit-* 相关响应头。

API 的具体错误字段可能随接口、SDK 版本或中间代理而变化,建议按以下顺序检查:

  1. error.code
  2. error.type
  3. HTTP 状态码和响应头
  4. error.message

Python SDK 中的处理方式

按照当前 Python SDK 的常见用法,所有 429 通常都会表现为 openai.RateLimitError,但异常对象一般还包含解析后的 code、type 或 body。

import openai

client = openai.OpenAI()

try:
    response = client.responses.create(
        model="gpt-5",
        input="Hello"
    )
except openai.RateLimitError as exc:
    error_code = getattr(exc, "code", None)
    error_type = getattr(exc, "type", None)

    body = getattr(exc, "body", None)
    if isinstance(body, dict):
        error = body.get("error", body)
        if isinstance(error, dict):
            error_code = error_code or error.get("code")
            error_type = error_type or error.get("type")

    if error_code == "insufficient_quota" or error_type == "insufficient_quota":
        raise RuntimeError(
            "OpenAI API quota is unavailable; retrying will not resolve it."
        ) from exc

    # 其他 429 可以交给上层重试机制处理
    raise

不同版本的 SDK 可能以不同方式暴露异常响应。调试时可以记录 exc.code、exc.type 和 exc.body,但不要记录 API key、完整请求内容或其他敏感信息。

配合 Tenacity 只重试可恢复错误

使用 Tenacity 时,不应重试所有 RateLimitError。需要提供一个判断函数,明确排除 insufficient_quota。

import logging
import openai

from tenacity import (
    retry,
    retry_if_exception,
    stop_after_attempt,
    wait_random_exponential,
)

logger = logging.getLogger(__name__)

client = openai.OpenAI()


def get_openai_error_fields(exc):
    code = getattr(exc, "code", None)
    error_type = getattr(exc, "type", None)
    message = str(exc)

    body = getattr(exc, "body", None)
    if isinstance(body, dict):
        error = body.get("error", body)
        if isinstance(error, dict):
            code = code or error.get("code")
            error_type = error_type or error.get("type")
            message = error.get("message") or message

    return code, error_type, message


def is_retryable_openai_error(exc):
    if isinstance(exc, openai.RateLimitError):
        code, error_type, message = get_openai_error_fields(exc)

        if code == "insufficient_quota" or error_type == "insufficient_quota":
            return False

        # 仅作为缺少结构化字段时的兼容措施
        normalized_message = message.lower()
        if (
            code is None
            and error_type is None
            and "exceeded your current quota" in normalized_message
        ):
            return False

        return True

    # 服务器端暂时性错误通常可以重试
    if isinstance(exc, openai.APIStatusError):
        return exc.status_code >= 500

    # 网络连接错误通常可以重试
    return isinstance(exc, openai.APIConnectionError)


@retry(
    retry=retry_if_exception(is_retryable_openai_error),
    wait=wait_random_exponential(min=1, max=30),
    stop=stop_after_attempt(6),
    reraise=True,
)
def call_openai():
    return client.responses.create(
        model="gpt-5",
        input="Hello"
    )


try:
    result = call_openai()
except openai.RateLimitError as exc:
    code, error_type, _ = get_openai_error_fields(exc)

    if code == "insufficient_quota" or error_type == "insufficient_quota":
        logger.error("OpenAI API quota unavailable; request was not retried.")
    else:
        logger.error("OpenAI rate limit remained active after retries.")

    raise

这里的 reraise=True 很重要。当重试条件返回 False 时,原始异常会立即向上传递,不会被包装成难以判断的重试异常。

即使错误可以重试,也必须设置 stop_after_attempt 或 stop_after_delay。无限重试会增加延迟,失败请求还可能计入某些限制窗口。

额度不足时应检查什么

Dashboard 显示”没有超过 hard limit”,并不能排除 insufficient_quota。还需要检查:

  • API 计费是否已启用,付款方式是否有效。
  • 预付余额或 API credits 是否已经耗尽或过期。
  • API key 是否属于预期的 project 或 organization。
  • 当前 project 是否设置了单独的预算或使用限制。
  • 免费试用额度是否已过期。
  • 修改计费设置后,旧 key 或项目配置是否仍然指向原来的账户范围。

ChatGPT 订阅和 API 计费通常是分开的。即使拥有 ChatGPT 付费套餐,也不代表 API 账户自动具有可用余额。

RPM 较低引发的是普通速率限制,与 insufficient_quota 不是一回事。如果响应明确给出 code: "insufficient_quota",应重点检查 API billing、credits、project 和 organization,而不是只降低请求频率。

不建议只匹配错误消息

下面的处理方式很脆弱:

if "quota" in str(exc).lower():
    ...

错误文案可能发生变化,也可能被本地化或由 SDK 包装。普通限流的消息中同样可能出现 quota。更可靠的检查顺序是:

error.code → error.type → status/headers → message fallback

如果响应确实没有结构化字段,可以通过完整且稳定的短语进行兼容匹配,同时记录脱敏后的响应体,方便后续确认。消息匹配只应作为降级方案,不能作为主要判断依据。

备注:内容仅供参考。