错误码与排查
错误封装格式、全部错误码,以及各类错误的重试策略。
错误封装#
错误封装按入站协议给出。OpenAI 协议(/v1/chat/completions 等):
{ "error": { "message": "账户余额不足", "type": "insufficient_quota", "code": "insufficient_balance", "param": "model" }, "request_id": "01JQ8Z3P9K7V2M4X6B8N0C1D3E"}Anthropic 协议(/v1/messages):
{ "type": "error", "error": { "type": "invalid_request_error", "message": "账户余额不足" }, "request_id": "01JQ8Z3P9K7V2M4X6B8N0C1D3E"}火山方舟视频协议(/api/v3/contents/generations/tasks)成功时响应形状跟方舟一致;失败仍走网关统一错误处理,排查请带上 X-Request-Id / request_id。状态字为 queued / running / succeeded / failed / cancelled,与 OpenAI 视频的 in_progress / completed 不同,见 视频生成 · 轮询。
code是机器可读的稳定标识,请以它为分支依据。注意 Anthropic 协议里没有code——那条端点上只能按 HTTP 状态与error.type分支。type是为兼容 SDK 的重试逻辑而映射的官方取值,不要用它区分具体原因。param只在参数类错误上出现,给出出错的字段名。request_id与响应头X-Request-Id一致,报障时提供它即可直达明细。- 更细的结构化信息(命中的限流键、被剔除的线路、上游状态码等)不在响应体里,按
request_id去 调用日志 查。
错误码总表#
鉴权与令牌
请求与参数
幂等键(带了 Idempotency-Key 才会出现)
计费与额度
限流
路由与上游
客户端与内部
重试策略#
不应对所有错误采用统一的重试策略。下列三档按「重发整个请求是否有意义」划分。
empty_completion 则相反:该错误不计费,直接重试没有额外成本。Python:只重试该重试的,并且尊重 Retry-After
import random, time, requests
# 429 是限流,5xx 是平台或上游的临时故障:这两类值得重试。# 4xx 里的其余错误是请求本身有问题,重试多少次结果都一样。RETRIABLE = {429, 500, 502, 503, 504}
def call_with_retry(url, body, headers, attempts=4): for attempt in range(attempts): response = requests.post(url, json=body, headers=headers, timeout=120) if response.status_code not in RETRIABLE: return response if attempt == attempts - 1: break
# 服务端说了等多久就等多久,没说才自己退避。 # 随机抖动是为了避免一批客户端在同一毫秒一起重来。 wait = response.headers.get("Retry-After") delay = float(wait) if wait else (2**attempt) + random.random() time.sleep(delay)
return responseNode.js:同样的重试策略
// 429 是限流,5xx 是平台或上游的临时故障:这两类值得重试。// 4xx 里的其余错误是请求本身有问题,重试多少次结果都一样。const RETRIABLE = new Set([429, 500, 502, 503, 504]);
async function callWithRetry(url, body, headers, attempts = 4) { let response;
for (let attempt = 0; attempt < attempts; attempt += 1) { response = await fetch(url, { method: 'POST', headers: { ...headers, 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); if (!RETRIABLE.has(response.status)) return response; if (attempt === attempts - 1) break;
// 服务端给了 Retry-After 就按它等,没给才自己退避; // 随机抖动用于避免一批客户端在同一时刻一起重来。 const retryAfter = response.headers.get('Retry-After'); const delay = retryAfter ? Number(retryAfter) : 2 ** attempt + Math.random(); await new Promise((resolve) => setTimeout(resolve, delay * 1000)); }
return response;}Java:只重试该重试的,并且尊重 Retry-After
import java.net.http.HttpClient;import java.net.http.HttpRequest;import java.net.http.HttpResponse;import java.util.Set;
// 429 是限流,5xx 是平台或上游的临时故障:这两类值得重试。// 4xx 里的其余错误是请求本身有问题,重试多少次结果都一样。static final Set<Integer> RETRIABLE = Set.of(429, 500, 502, 503, 504);
static HttpResponse<String> callWithRetry(HttpRequest request, int attempts) throws Exception { HttpClient http = HttpClient.newHttpClient(); HttpResponse<String> response = null;
for (int attempt = 0; attempt < attempts; attempt++) { response = http.send(request, HttpResponse.BodyHandlers.ofString()); if (!RETRIABLE.contains(response.statusCode())) { return response; } if (attempt == attempts - 1) { break; }
// 服务端给了 Retry-After 就按它等,没给才自己退避; // 随机抖动用于避免一批客户端在同一时刻一起重来。 double delay = response.headers().firstValue("Retry-After") .map(Double::parseDouble) .orElse(Math.pow(2, attempt) + Math.random()); Thread.sleep((long) (delay * 1000)); }
return response;}限流#
限流有 RPM、TPM、并发三个维度。RPM 与 TPM 按 API Key 与账户两级判定,并发按账户判定。
- RPM 与 TPM 超限都返回
rate_limited(429),并发占满返回concurrency_limited(429)。 - TPM 按估算的输入 token 计量;真实用量要在上游返回后才可知。
- 429 一律带
Retry-After(秒,最小值为 1)。请按该值退避;不遵守退避会延长被限流的时间。 - 流式长连接会持续占用并发位直到流结束,因此并发限额往往比预期更早触发。
失败请求的排查步骤#
- 1
拿到 request_id
响应头
X-Request-Id,或错误体里与error同级的request_id,两者一致。 - 2
在调用日志里搜它
调用日志 支持按 request_id 精确查,能看到请求参数摘要、路由轨迹与计费明细。
- 3
看路由轨迹
路由轨迹列出每次尝试所用的线路、上游状态码与耗时。
all_candidates_failed基本都能据此定位到失败环节。 - 4
仍无法定位时提交工单
携带 request_id 提交 工单 即可,无需附上完整请求体。
/v1/chat/completions 会返回 400,并提示改用 POST /v1/videos。no_candidate 绝大多数并非「目录中没有该模型」,而是 provider 的约束把所有线路都筛除了——请查阅 调用日志 中的路由轨迹,其中记录了每条线路被哪个条件筛除。