JuCode 文档

错误码

完整错误码表,以及哪些该重试、哪些不该。

错误信封

/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_model400model 字段,或它不是字符串
invalid_json400请求体不是合法 JSON
read_body_failed400读取请求体失败
invalid_multipart400应为 multipart/form-data
file_read_failed400读取上传文件失败
invalid_id400任务 ID 格式非法
task_not_found404视频任务不存在,或不属于当前账号
request_body_too_large413对话端点上限 100 MiB;multipart 端点 32 MiB

认证与权限

code状态含义
invalid_api_key401未提供凭证 / Key 无效或已吊销 / OAuth 令牌无效或过期
expired401API Key 已过期
missing_user401内部状态异常,请报障
account_disabled403账号非活跃
account_frozen403因违规临时冻结
access_denied403无权访问该模型或模型组

排查细节见认证

路由

code状态含义
model_not_found400模型名不存在
model_group_unavailable400该模型对当前 Key 不可用
no_available_provider503该模型当前无可用上游
endpoint_unsupported_by_providers503没有上游支持该端点——通常是模型选错了
async_not_implemented501异步任务存储不可用,视频端点暂不可用

计费

配额类问题统一是 type: "billing_error"code: "billing_rejected",靠状态码 区分:

状态含义
402余额不足等一般计费失败
403当前套餐不允许此操作
429配额耗尽、月度消费超限、Key 成本超限、组织或成员额度超限
503计费来源不受支持

具体原因在 message 里。

内容审核

code状态含义附加字段
input_policy_block422请求内容被拦截error.categories(数组)
output_policy_block422模型输出被拦截(非流式)error.category(字符串)
cyber_safety_block403被安全策略拦截

注意 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_unreachable502连不上上游
upstream_unavailable502 / 503所有上游均失败
pool_exhausted503连接池耗尽
rate_limited503上游限流
upstream_rejected上游 4xx上游拒绝了请求
context_length_exceeded上游 4xx上下文超长
invalid_request上游 4xx上游认为请求格式有误
feature_not_enabled上游 4xx上游账号未开通该能力
client_version_unsupported上游 4xx客户端版本过低
video_submit_failed502视频任务提交失败

超时

全部是 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": "..."
}

只能靠 typemessage 判断。

格式例外

有两处不遵循上面任何一种信封,如果你写了统一的错误解析,这两处要单独处理:

/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))  # 加抖动,避免同步重试风暴

On this page