错误码
完整错误码表,以及哪些该重试、哪些不该。
错误信封
/v1/* 端点的错误统一是这个形状:
{
"error": {
"type": "invalid_request_error",
"code": "missing_model",
"message": "missing model",
"request_id": "3f8a1c2e-5b4d-4e6f-9a0b-1c2d3e4f5a6b"
},
"request_id": "3f8a1c2e-5b4d-4e6f-9a0b-1c2d3e4f5a6b"
}request_id 在顶层和 error 里各出现一次,值相同,方便不同 SDK 提取。同一个值也在
X-Request-Id 响应头里。报障时请附上它——没有它基本无法定位单次请求。
按 code 判断,不要按 type
type 是粗分类,存在反直觉的映射。最典型的是账号被禁用/冻结返回 403,但
type 仍是 authentication_error 而非 permission_error。
重试策略、用户提示,都应该基于 code。
有两个例外不遵循这个信封,见页面末尾的格式例外。
该不该重试
| 状态码 | 该重试吗 | 说明 |
|---|---|---|
400 | 否 | 请求本身有问题,重试结果一样 |
401 403 | 否 | 凭证或权限问题,先修配置 |
404 | 否 | — |
413 | 否 | 先缩小请求体 |
422 | 否 | 内容审核拦截,换措辞而非重试 |
429 | 看 code | 见下文 |
500 | 否 | 内部错误,带 request_id 报障 |
502 | 是 | 传输层失败,通常瞬时 |
503 | 是 | 遵守 Retry-After |
504 | 是 | 超时,建议退避后重试 |
429 要分情况
| code | 该重试吗 | 正确做法 |
|---|---|---|
concurrency_limit | 是 | 这是瞬时并发数超限,不是 QPS。等已有请求返回再发,而不是盲目退避 |
billing_rejected | 否 | 配额/余额问题,重试无用,需要充值或调整限额 |
这两个都返回 429 且都不带 Retry-After,只能靠 code 区分。详见
限流与并发。
完整错误码表
请求校验
| code | 状态 | 含义 |
|---|---|---|
missing_model | 400 | 缺 model 字段,或它不是字符串 |
invalid_json | 400 | 请求体不是合法 JSON |
read_body_failed | 400 | 读取请求体失败 |
invalid_multipart | 400 | 应为 multipart/form-data |
file_read_failed | 400 | 读取上传文件失败 |
invalid_id | 400 | 任务 ID 格式非法 |
task_not_found | 404 | 视频任务不存在,或不属于当前账号 |
request_body_too_large | 413 | 对话端点上限 100 MiB;multipart 端点 32 MiB |
认证与权限
| code | 状态 | 含义 |
|---|---|---|
invalid_api_key | 401 | 未提供凭证 / Key 无效或已吊销 / OAuth 令牌无效或过期 |
expired | 401 | API Key 已过期 |
missing_user | 401 | 内部状态异常,请报障 |
account_disabled | 403 | 账号非活跃 |
account_frozen | 403 | 因违规临时冻结 |
access_denied | 403 | 无权访问该模型或模型组 |
排查细节见认证。
路由
| code | 状态 | 含义 |
|---|---|---|
model_not_found | 400 | 模型名不存在 |
model_group_unavailable | 400 | 该模型对当前 Key 不可用 |
no_available_provider | 503 | 该模型当前无可用上游 |
endpoint_unsupported_by_providers | 503 | 没有上游支持该端点——通常是模型选错了 |
async_not_implemented | 501 | 异步任务存储不可用,视频端点暂不可用 |
计费
配额类问题统一是 type: "billing_error"、code: "billing_rejected",靠状态码
区分:
| 状态 | 含义 |
|---|---|
| 402 | 余额不足等一般计费失败 |
| 403 | 当前套餐不允许此操作 |
| 429 | 配额耗尽、月度消费超限、Key 成本超限、组织或成员额度超限 |
| 503 | 计费来源不受支持 |
具体原因在 message 里。
内容审核
| code | 状态 | 含义 | 附加字段 |
|---|---|---|---|
input_policy_block | 422 | 请求内容被拦截 | error.categories(数组) |
output_policy_block | 422 | 模型输出被拦截(非流式) | error.category(字符串) |
cyber_safety_block | 403 | 被安全策略拦截 | — |
注意 categories(复数,输入)和 category(单数,输出)是两个不同字段。
流式下的输出拦截不返回 422
流式请求的响应头在第一帧之前就已发出,状态码无法再改。所以输出被拦截时,网关会在 SSE 流中注入一个终止帧:
data: {"id":"policy-blocked","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"content_filter"}],"x_jucode_block":{"category":"...","severity":"..."}}
data: [DONE]也就是说 HTTP 状态仍是 200,流也正常结束。要检测这种情况,得判断
finish_reason == "content_filter",或者检查 x_jucode_block 字段的存在。
Anthropic 端点上则是以 end_turn 正常收尾——刻意不报错,以免触发 SDK 自动重试。
上游
| code | 状态 | 含义 |
|---|---|---|
upstream_unreachable | 502 | 连不上上游 |
upstream_unavailable | 502 / 503 | 所有上游均失败 |
pool_exhausted | 503 | 连接池耗尽 |
rate_limited | 503 | 上游限流 |
upstream_rejected | 上游 4xx | 上游拒绝了请求 |
context_length_exceeded | 上游 4xx | 上下文超长 |
invalid_request | 上游 4xx | 上游认为请求格式有误 |
feature_not_enabled | 上游 4xx | 上游账号未开通该能力 |
client_version_unsupported | 上游 4xx | 客户端版本过低 |
video_submit_failed | 502 | 视频任务提交失败 |
超时
全部是 504,type: "upstream_error":
| code | 含义 |
|---|---|
first_text_timeout | 上游迟迟未返回首个 token |
stream_total_timeout | 流式响应总时长超限 |
stream_idle_timeout | 流式响应中途停止推送 |
stream_incomplete | 流在完成前意外结束 |
upstream_timeout | 非流式请求超时(默认 120 秒) |
非流式的 120 秒上限不适用于流式请求。如果你的任务本来就慢,用流式能绕开它。
上游状态码怎么映射
这段决定了你的重试逻辑是否正确:
- 上游 429 → 网关返回 503 +
Retry-After - 上游 5xx → 网关返回 503 +
Retry-After - 上游 4xx(非 429)→ 原样透传该状态码
- 传输层失败 → 502
- 超时 → 504
上游的 429 不会以 429 返回给你
这是有意为之:429 语义上是"你超限了",而这里是"上游超限了"。混在一起会让客户端 误以为要减少自己的请求量。
唯一例外是 /anthropic/v1/messages ——它保留 429,以便 Anthropic SDK 内建的退避
重试正常生效。
Retry-After
只在上游限流、上游 5xx、连接池耗尽这三种情况下返回,始终是整数秒且被钳制在 1–60 之间。上游给出的建议值会被优先采用,但同样受这个区间限制。
concurrency_limit 的 429 和模型列表端点的 429 都不带这个头。
Anthropic 端点的错误格式
/anthropic/v1/messages 用 Anthropic 信封,没有 code 字段:
{
"type": "error",
"error": {
"type": "invalid_request_error",
"message": "missing model",
"request_id": "..."
},
"request_id": "..."
}只能靠 type 和 message 判断。
格式例外
有两处不遵循上面任何一种信封,如果你写了统一的错误解析,这两处要单独处理:
/v1/models 和 /anthropic/v1/models 超限时(429)
{"error": "rate limit exceeded", "scope": "user"}error 是字符串,不是对象。
/v1/models 服务端错误时(500)
{"error": "<原始错误信息>"}同样是字符串。
推荐的重试实现
import time
import random
RETRYABLE = {502, 503, 504}
def call_with_retry(fn, max_attempts=4):
for attempt in range(max_attempts):
resp = fn()
if resp.status_code < 400:
return resp
body = resp.json()
code = (body.get("error") or {}).get("code") if isinstance(body.get("error"), dict) else None
# 并发超限:等待而非放弃,但退避要短
if resp.status_code == 429 and code == "concurrency_limit":
delay = 1.0
elif resp.status_code in RETRYABLE:
# 优先采纳服务端建议
delay = float(resp.headers.get("Retry-After", 2 ** attempt))
else:
return resp # 其余一律不重试
if attempt == max_attempts - 1:
return resp
time.sleep(delay + random.uniform(0, 0.3)) # 加抖动,避免同步重试风暴