gpt-5.6-sol 一直报 429 但 gpt-5.5 正常怎么办?两个限流桶要分开退避才行
上周三我在 Cline 里跑一个代码的 pipeline,把模型从 gpt-5.5 切到 gpt-5.6-sol 之后,同样的请求量,429 像机关枪一样往外蹦。一开始以为是额度用完了,充了钱发现还是一样。折腾半天才搞明白:gpt-5.6-sol 的 RPM(每分钟请求数)和 TPM(每分钟 Token 数)是两个独立的限流桶,而且这两个桶的阈值比 gpt-5.5 低不少。退避逻辑必须分桶处理,不能一个 sleep 打天下。另外 429 其实藏着两种完全不同的子类型:rate_limit_exceeded(等一等就好)和 insufficient_quota(钱花完了,重试一万次也没用),混淆这俩是最常见的浪费时间的坑。
为什么 gpt-5.6-sol 比 gpt-5.5 更容易触发 429
先看一个实际收到的报错:
openai.RateLimitError: Error code: 429 -
{'error': {'message': 'Rate limit reached
for model `gpt-5.6-sol` on tokens per min
(TPM): Limit 10000, Used 9800, Requested
500. Please try again in 1.8s.',
'type': 'tokens', 'code':
'rate_limit_exceeded'}}
关键信息都在这条报错里了。type: 'tokens' 说明触发的是 TPM 桶,不是 RPM 桶。消息里直接给了三个数字:Limit 10000 / Used 9800 / Requested 500,还告诉你等 1.8s。注意 error.type 的实际值可能因 SDK 版本和代理层而有差异,建议以实际响应为准。
但问题来了------同样的账户 Tier,gpt-5.5 的 TPM 上限可能是 30000,gpt-5.6-sol 只给 10000。OpenAI 不同模型的速率限制是独立配置的,新模型上线初期限制通常更紧。你可以在 platform.openai.com/account/limits 看到自己当前 Tier 下每个模型的具体数字。
429 的两种子类型:先分清再动手
这步最重要,搞反了后面全白费。
| 子类型 | error.type | error.code | 该怎么办 | 重试有用吗 |
|---|---|---|---|---|
| 速率超限 | tokens 或 requests |
rate_limit_exceeded |
按响应头等待后重试 | ✅ 有用 |
| 配额耗尽 | invalid_request_error |
insufficient_quota |
去 platform.openai.com 充值 | ❌ 重试无意义 |
注意:配额耗尽时,OpenAI 实际返回的 error.type 是 "invalid_request_error","insufficient_quota" 出现在 error.code 字段,两者不要混用。
实际报错长这样(配额耗尽版):
openai.RateLimitError: Error code: 429 -
{'error': {'message': 'You exceeded your
current quota, please check your plan and
billing details.', 'type':
'invalid_request_error', 'code':
'insufficient_quota'}}
注意看------这条没有"Please try again in Xs",因为等多久都没用。第一次遇到的时候很容易傻乎乎地让重试循环跑好几轮,每轮等几十秒,白白浪费时间才报最终异常。
RPM 桶和 TPM 桶:两套响应头,两套退避策略
OpenAI 在每个响应(包括 429 响应)里塞了两组限流头,分别对应 RPM 和 TPM:
| 响应头 | 含义 |
|---|---|
x-ratelimit-limit-requests |
RPM 桶总容量 |
x-ratelimit-remaining-requests |
RPM 桶剩余 |
x-ratelimit-reset-requests |
RPM 桶距重置的剩余时间(字符串,如 "6m0s") |
x-ratelimit-limit-tokens |
TPM 桶总容量 |
x-ratelimit-remaining-tokens |
TPM 桶剩余 |
x-ratelimit-reset-tokens |
TPM 桶距重置的剩余时间(字符串,如 "1.8s") |
特别注意:x-ratelimit-reset-requests 和 x-ratelimit-reset-tokens 返回的是相对时间字符串 (如 "1s"、"1.8s"、"6m0s"),而不是 Unix 时间戳或整数秒数。不能直接把它塞进 time.sleep(),需要先解析。下面是一个简单的解析函数:
python
import re
def parse_reset_time(reset_str: str) -> float:
"""把 '1.8s' / '6m0s' / '1m30s' 解析成秒数(float)"""
if reset_str is None:
return 0.0
minutes = re.search(r'(\d+)m', reset_str)
seconds = re.search(r'([\d.]+)s', reset_str)
total = 0.0
if minutes:
total += float(minutes.group(1)) * 60
if seconds:
total += float(seconds.group(1))
return total
gpt-5.6-sol 的坑在于:这两个桶的阈值都比 gpt-5.5 低,而且是独立触发的。你可能 RPM 还有余量,但 TPM 已经爆了------反过来也一样。所以退避策略不能只看一个维度。
先把这两个值读出来。在 openai-python v1.x 中,RateLimitError 的响应头通过 e.response.headers 访问(e.response 是一个 httpx.Response 对象):
python
import openai
client = openai.OpenAI()
python
print("RPM remaining:",
resp.headers.get(
"x-ratelimit-remaining-requests"))
print("TPM remaining:",
resp.headers.get(
"x-ratelimit-remaining-tokens"))
reset_tokens_str = resp.headers.get(
"x-ratelimit-reset-tokens")
print("TPM reset in:",
parse_reset_time(reset_tokens_str), "秒")
具体数字因 Tier 而异,去 platform.openai.com/account/limits 自己查一眼最准。
方案一:手动指数退避 + jitter(最小依赖)
最基础的做法,不引入额外库。关键改动:先判断 insufficient_quota 直接抛出不重试;读取响应头中的实际重置时间,取响应头等待时间与指数退避时间的较大值,再叠加 jitter 避免多个并发请求同时醒来撞车。
python
import openai, time, random, re
def parse_reset_time(reset_str):
if reset_str is None:
return 0.0
minutes = re.search(r'(\d+)m', reset_str)
seconds = re.search(r'([\d.]+)s', reset_str)
total = 0.0
if minutes:
total += float(minutes.group(1)) * 60
if seconds:
total += float(seconds.group(1))
return total
client = openai.OpenAI()
python
for attempt in range(6):
try:
resp = client.chat.completions.create(
model="gpt-5.6-sol",
messages=[{"role": "user",
"content": "hi"}])
break
except openai.RateLimitError as e:
if "insufficient_quota" in str(e):
raise # 没钱了,别重试
# 从 e.response.headers 读取实际重置时间
# (openai-python v1.x,e.response 为 httpx.Response)
headers = getattr(
getattr(e, 'response', None),
'headers', {}) or {}
header_wait = max(
parse_reset_time(
headers.get(
"x-ratelimit-reset-tokens")),
parse_reset_time(
headers.get(
"x-ratelimit-reset-requests")))
backoff = 2 ** attempt
wait = max(header_wait, backoff)
wait += random.uniform(0, wait * 0.5)
time.sleep(wait)
random.uniform(0, wait * 0.5) 是比例式 jitter------在等待时间基础上随机叠加 0~50% 的偏移。没有它的话,你开 10 个并发,第一轮全等 1s,第二轮全等 2s------它们永远同时醒来同时撞限流墙,根本退不开。
方案二:tenacity 装饰器(生产环境推荐)
手写 for 循环容易漏边界条件。tenacity 可以简化重试逻辑:
python
from tenacity import (retry,
wait_exponential_jitter,
stop_after_attempt,
retry_if_exception)
import openai
python
def is_retryable(e):
# insufficient_quota 不重试;其他 RateLimitError 重试
return (isinstance(e, openai.RateLimitError)
and "insufficient_quota" not in str(e))
@retry(
wait=wait_exponential_jitter(
initial=1, max=60, jitter=5),
stop=stop_after_attempt(6),
retry=retry_if_exception(is_retryable))
def call_sol():
return openai.OpenAI().chat.completions\
.create(model="gpt-5.6-sol",
messages=[{"role": "user",
"content": "hi"}])
这里有两点需要说明:
-
is_retryable已内置 :直接用retry_if_exception(is_retryable)替代retry_if_exception_type,同时过滤掉insufficient_quota,不需要再单独处理。 -
jitter 参数含义 :
wait_exponential_jitter的jitter=5表示在指数退避基础上额外叠加 0~5 秒的均匀随机量 ,而不是按比例抖动。这与方案一的random.uniform(0, wait * 0.5)(比例式 jitter,随请求量增大而增大)策略不同------前者抖动幅度固定,后者抖动幅度随退避时间等比放大。高并发场景下比例式 jitter 通常分散效果更好;如果并发数不多,固定 jitter 已经够用。另外,wait_exponential_jitter的multiplier默认值为 1,退避曲线相对平缓,生产环境如果需要更激进的退避可以适当调大。
方案三:用聚合网关分散限流压力
上面两个方案解决的是单 Key 下的退避问题。如果业务量确实大,单一 Key 的 RPM/TPM 桶迟早不够用,这时候可以考虑通过 API 聚合平台把请求分发到多个通道。
OpenRouter 和 ofox.io 都支持 OpenAI 兼容协议,改一个 base_url 参数即可接入;OpenRouter 对部分模型收取溢价,ofox.io 宣称 0% 加价对齐官方价格(商业声明,建议自行核实当前定价)。配置示例如下:
python
# OpenRouter 示例
client_or = openai.OpenAI(
api_key="your-openrouter-key",
base_url="https://openrouter.ai/api/v1")
# ofox.io 示例
client_fox = openai.OpenAI(
api_key="your-ofox-key",
base_url="https://api.ofox.io/v1")
python
resp = client_fox.chat.completions.create(
model="gpt-5.6-sol",
messages=[{"role": "user",
"content": "这段代码"}])
这类聚合网关的好处是后端有多条通道做负载均衡,单条通道触发限流时自动切到下一条,业务层不用关心退避逻辑。OpenRouter 也能做类似的事,看你更在意手续费还是模型覆盖面。具体后台功能和定价策略建议以各平台官网当前说明为准。
一个容易忽略的前置检查
社区 QA 里有人提到:如果你用的模型名不存在,某些 SDK 版本或代理层可能把 404 包装成 429 或其他错误码。排查 429 之前,先跑一下这个确认模型名有效:
python
import openai
models = openai.OpenAI().models.list()
sol_exists = any(
m.id == "gpt-5.6-sol" for m in models)
print(f"gpt-5.6-sol exists: {sol_exists}")
如果返回 False,那你的 429 可能根本不是限流问题。
常见问题 FAQ
Q: 429 报 insufficient_quota 和 rate_limit_exceeded 到底怎么区分?
看 error.code 字段。insufficient_quota 是账户余额/配额耗尽,去 platform.openai.com 充值;rate_limit_exceeded 是速率超限,等一等或降低并发就行。最直观的区别:后者的错误消息里会给 "Please try again in Xs",前者不会。另外注意,配额耗尽时 error.type 是 "invalid_request_error","insufficient_quota" 出现在 error.code,不要把两个字段的值混用。
Q: 怎么判断是 TPM 还是 RPM 触发了 429?
看 error.type。'tokens' 就是 TPM 桶爆了,'requests' 就是 RPM 或 RPD 桶爆了。错误消息里也会明确写 "tokens per min (TPM)" 或 "requests per day (RPD)"。实际值可能因 SDK 版本和代理层有差异,以实际响应为准。
Q: gpt-5.6-sol 的限流阈值具体是多少?
因 Tier 而异,OpenAI 没有公开每个模型每个 Tier 的完整限流表。只能在 platform.openai.com/account/limits 看自己账户的实际值,以官网显示为准。
Q: 多个并发请求同时重试怎么办?
加 jitter。指数退避的等待时间上叠加一个随机偏移量,让并发请求的唤醒时间错开。不加 jitter 的指数退避在高并发下基本等于没退避。具体可以用比例式(random.uniform(0, wait * 0.5))或固定量(tenacity 的 jitter=5),两种策略的区别见方案二的说明。
Q: 升级 Tier 需要什么条件?
Tier 不是单纯靠充值金额决定的,账户存在天数也是必要条件。根据 OpenAI 官方文档,各 Tier 门槛大致如下:
- Tier 1:支付 $5 且账户存在 ≥ 7 天
- Tier 2:累计消费 $50 且账户存在 ≥ 14 天
- Tier 3~5:更高消费门槛 + 更长账户存在天数(具体数字 OpenAI 历史上多次调整,以 platform.openai.com/docs/guides/rate-limits 官网当前值为准)
也就是说,光充钱不够,账户还得"养"够对应天数才能升级。OpenAI 可能随时调整这些门槛,务必以官网为准。
小结
gpt-5.6-sol 报 429 大概率不是代码有 bug,而是这个模型的限流桶比 gpt-5.5 紧很多。排查顺序:先确认模型名存在,再区分 insufficient_quota 还是 rate_limit_exceeded,然后读响应头判断是 RPM 桶还是 TPM 桶(注意重置时间是字符串格式,需要解析),最后针对性地退避。生产环境用 tenacity + jitter,业务量大就上聚合网关做多通道负载均衡。目前 gpt-5.6-sol 的限流就是比 gpt-5.5 紧,先把退避逻辑写对,比什么都强。