Gemini 撞到配额上限时,返回体固定是这一套:
{
"error": {
"code": 429,
"message": "Resource has been exhausted (e.g. check quota).",
"status": "RESOURCE_EXHAUSTED"
}
}
code 是 429,status 是 RESOURCE_EXHAUSTED,这两行说明请求本身没写错、key 也没问题,纯粹是单位时间内的量超了。它和你代码里的 bug 无关,所以别去改 prompt,改的是发送节奏。
配额策略配置得比较细的项目,message 里会直接点名:
{
"error": {
"code": 429,
"message": "Quota exceeded for quota metric 'Generate Content API requests per minute' and limit 'GenerateContentRequestsPerMinutePerProjectPerRegion' of service 'generativelanguage.googleapis.com'",
"status": "RESOURCE_EXHAUSTED",
"details": [
{"@type": "type.googleapis.com/google.rpc.RetryInfo", "retryDelay": "17s"}
]
}
}
RetryInfo 里的 retryDelay 是服务端算出来的等待时间,按它来退避比拍脑袋定值靠谱得多。
OpenAI 兼容客户端往往只显示 Rate limit reached 或者 当前分组上游负载已饱和。看到这类提示,要回到原始响应里翻 status 字段,或者直接在命令行复现一次,才能确认到底是不是 429。
区分的办法很简单:立刻重试就能过是 RPM;隔几分钟才缓过来是 RPD;只有长 prompt 批量任务才红是 TPM。
这就是为什么换了 key 还是红——同一个项目下的多把 key 共享同一个配额池,真正分开计算的只有不同项目。多 key 轮询能兜住的只有短时并发,救不了 RPD 撞满的情况。
线程池开 50 个并发去丢请求,实际能通过的量由上游配额决定,多出来的部分会全部堆成 429。先算出目标速率(例如 RPM 上限是 200,就按每秒 3 个请求排队),再让线程数与之匹配,比事后加 sleep 有效得多。
curl -s -X POST \
"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent" \
-H "x-goog-api-key: $GEMINI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"contents":[{"parts":[{"text":"ping"}]}]}' | jq '.error'
把 .error.message 和 .error.details 一起打出来,就知道是每分钟还是每天被卡住。
抓一段时间的请求日志,按分钟和按天分别聚合,得到峰值 RPM 和全天总量。批量任务不要只看平均值,峰值那几分钟才决定你会不会撞墙。
把算出来的每分钟上限乘以 0.7 到 0.8 作为安全余量,落到客户端的限流器上。剩下的余量留给突发和重试,不然重试本身就会把配额吃干净。
import asyncio
from openai import AsyncOpenAI
client = AsyncOpenAI(api_key="你的平台KEY", base_url="https://xyuapi.top/v1")
sem = asyncio.Semaphore(4) # 同时不超过 4 个在飞
interval = 0.25 # 每个请求的间隔下限,约 240 RPM
async def call_once(prompt: str, model: str = "gemini-2.5-pro"):
async with sem:
await asyncio.sleep(interval)
r = await client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
timeout=180,
)
return r.choices[0].message.content
并发和间隔两个参数一起调,比只调其中一个更稳。信号量管住瞬时并发,间隔管住长期速率。
两千条数据一次跑完,中途撞了 429 就得从头来。改成按 200 条一批,每批跑完写一次进度文件,重跑时先读进度文件跳过已完成的,整个任务就从「要么全成要么全废」变成了可中断可续跑。
成功写结果、失败写待重试队列,等主流程跑完再统一处理待重试的少量条目。这样主流程不会被个别慢请求拖死,重试也不会把刚缓过来的配额又打满。
待重试队列本身也要限速,它和主流程共用同一个并发信号量,才不会在主流程刚缓过来的时候又被重试挤爆。
进队列的条件要写清楚:429、503、超时进队列;400 参数错误、401 鉴权失败直接丢弃。参数写错重试一百次也是同样的结果,只会白白吃调用次数。
再加一块用量看板,把每分钟请求数画成曲线,撞墙的时间点一眼可见,调完参数能立刻验证效果,比凭感觉调参可靠得多。
一批线程同时撞墙,同时 sleep(5),5 秒后同时醒来再同时撞墙,永远在同一个节拍上打转。退避必须带随机抖动,把大家的重试时间错开。
import random, time
def retry_with_backoff(fn, attempts: int = 6, base: float = 1.0, cap: float = 60.0):
for i in range(attempts):
try:
return fn()
except Exception as e:
if "429" not in str(e) and "RESOURCE_EXHAUSTED" not in str(e):
raise
wait = min(cap, base * (2 ** i)) * (0.5 + random.random() * 0.5)
time.sleep(wait)
raise RuntimeError("重试次数用尽")
返回体里有 RetryInfo 时,直接用它给的值加一点抖动,通常比指数退避算出来的更贴近真实窗口。只有拿不到 details 的时候,才退回纯本地退避。
连续三次以上 429 就该停下来,把并发降到一半再继续,而不是无休止重试。重试是有成本的:它吃配额、吃时间,还可能把一次小拥堵放大成整体不可用。
同一批要处理的内容,能合进一次调用就别拆成十次。合成一条带编号的清单,让模型一次输出十条结果,再按编号切分。请求数直接降到十分之一,配额问题大半消失。
合并的时候把输出格式写死:每行以编号开头,字段之间用固定分隔符。解析时按行切分再按编号对齐,哪一条缺失一目了然,重跑时只补缺的那几条即可。
注意别把合并做过头。一次塞进去五十条,输出长到触发长度上限,反而会出现截断和错位。实践上二十到三十条一批是比较稳的区间,具体看单条内容的长度。
翻译、分类、摘要这类任务,同样的输入重复出现的概率很高。把输入的哈希当 key,结果落本地文件或 SQLite,命中就直接返回,既省配额也快。
先用 gemini-2.5-pro 跑全量,只把模型自己标记为「不确定」的少量样本交给 gemini-3.1-pro-preview 复核。这样既保住质量,又把高配额消耗的请求数压到很低。
这个思路在成本上也算得清:gemini-2.5-pro 单次 0.031 元,gemini-3.1-pro-preview 单次 0.09 元。一千条数据全部走后者接近九十元,先用前者全量跑、再挑出两成复核,总支出能压到原来的三成上下,而多数抽取类任务的质量差异小到看不出来。
复核提示词也要设计好:明确告诉模型只做判断、只输出结论与依据,不要重写全文。输出短、速度快、花费低,批量处理也更容易。
把复核结果落库,积累一段时间后回看被标记的样本,通常会发现打底模型的薄弱点集中在某一两类输入上,改这几类输入的提示词就行,不用整体推翻。
| 状态码与字段 | 常见 message 片段 | 真实含义 | 处理动作 |
|---|---|---|---|
| 429 | Resource has been exhausted | 配额撞顶 | 退避等待,降低并发 |
| 429 | Quota exceeded for quota metric ... per minute | RPM 或 TPM 超限 | 按秒级节流 |
| 503 | The model is overloaded | 上游临时过载 | 换模型或稍后重试 |
| 504 | Deadline exceeded | 请求超时被中断 | 加长超时并开流式 |
| 402 / 403 | 额度或权限提示 | 余额不足或权限未开 | 检查账户余额与模型权限 |
上游过载返回的 503 会自己恢复,退避一两秒再试就行;429 撞的是你自己的配额节奏,不降速就会一直红。两者的处理方式恰好相反,混为一谈只会白等。
如果每天固定时间大面积 429,那就不是运气问题,而是任务调度把高峰叠在了一起。错峰排班比加更多重试有效。
错峰的做法很具体:把批量任务挪到业务低谷时段,或者按项目把任务切成几个时间片,每个时间片单独限速。调整一次调度表,往往比在代码里多加十行重试逻辑都管用。
还要盯住客户端自带的自动重试开关。不少 SDK 默认会重试两到三次,再叠加你自己写的重试,一次失败实际会发出七八个请求,日志里看起来只是「偶发 429」,实际用量早就翻了倍。
主入口 https://xyuapi.top/v1,备用 https://xyuai.cc/v1,另支持 Gemini 原生 /v1beta 路径。OpenAI 兼容协议,客户端里填 Chatbox、Cherry Studio、NextChat、Cline 都能直接用。
平台按一次请求固定价收费,输入多长都是同一个价格。需要塞进长上下文的任务,成本不会因为输入翻倍而翻倍,做批量预算时按调用次数乘单价就能算出来。
| 模型 | 价格 | 适合的任务 |
|---|---|---|
| gemini-2.5-pro | 0.031 元/次 | 批量抽取、长文档总结 |
| gemini-3-pro-preview | 0.05 元/次 | 复杂推理、代码生成 |
| gemini-3.1-pro-preview | 0.09 元/次 | 高难度分析与复核 |
| claude-sonnet-4-5-thinking | 0.09 元/次 | 带思考链的长文处理 |
| NanoBanana-Pro(生图) | 0.27 元/次 | 图片生成与编辑 |
出问题时先在平台的用量页面对一下自己的统计,两边数字差得离谱就说明有客户端在偷偷重试,把重试收敛住,用量自然会跌回预期。
先用小批量(比如 50 条)把整套流程跑通,确认不再出现 429,再放大到全量。
把重试后仍然失败的请求单独存成文件,跑完回头看这批样本,多半能发现是某类超长输入或者某种特殊格式在拖后腿,修掉它比继续调参更值。