遇到问题
查报错
#贴上报错
读错误只按一个顺序:先看 HTTP 状态码,再看 error.type,最后看 error.code。状态码永远有意义;type 告诉你是谁拒绝了请求;只有网关自己抛出的错误,code 才是稳定的。
#如何分支处理
- 1
先按状态码分支
402 是去充值,403 是密钥权限,429 是等待,5xx 是重试。这一层不会骗你,也不会变。
- 2
再看 error.type
xiaoxiang_error 是我们的。其他值——openai_error、claude_error、gemini_error——都是厂商自己的响应体,原样透传。
- 3
最后才读 error.code
只对 xiaoxiang_error 按 code 分支。上游的 code 属于厂商,随时可能变。
并不是每个错误都有 JSON 响应体
错误结构(四种)
会到达客户端的一共五种结构。五种都要处理——后三种是接入时最常被漏掉的。
网关自身错误
请求被网关自身拒绝——认证、额度、路由或请求结构问题。
{
"error": {
"message": "insufficient account balance, please top up to continue (request id: 20260910...)",
"type": "xiaoxiang_error",
"code": "insufficient_user_quota"
}
}上游错误,原样透传
厂商返回了自己的错误。响应体原样转发,因此 type 和 code 属于厂商,不是我们的。
{
"error": {
"message": "This model's maximum context length is 128000 tokens.",
"type": "invalid_request_error",
"code": "context_length_exceeded"
}
}前置守卫拒绝
幂等等少数前置守卫在进入中继之前就返回了,此时还没有中继错误结构:扁平结构、大写 code。
{
"code": "IDEMPOTENCY_IN_PROGRESS",
"error": "a request with this Idempotency-Key is still processing"
}异步任务拒绝
所有任务端点——视频、图片任务、音乐、3D——都用自己的扁平结构报告失败。这里没有嵌套的 error 对象,因此按 /v1/chat/completions 写的解析逻辑在这里会读到 null。
{
"code": "task_not_exist",
"message": "task does not exist",
"data": null
}只有状态码,响应体为空
限流拒绝和请求体过大只返回一个状态行,没有响应体。
HTTP/1.1 429 Too Many Requests
Retry-After: 20
Content-Length: 0响应头
每一次响应都记录 request id,而不只是失败时。客服正是凭它追踪一次调用——有它是五分钟答复,没有它就是一整天的猜测。
| 请求头 | 何时发送 | 该怎么用 |
|---|---|---|
| X-Oneapi-Request-Id | 每一次响应,无论成功还是失败。 | 请和你自己的链路 ID 一起记录。客服正是凭这个值定位请求;网关的错误消息里也会附带同一个 ID。 |
| Retry-After | 出现 429 时,以及幂等键仍在处理中返回 409 时。 | 至少等待这么多秒。这是网关唯一给出的退避提示。 |
| X-Idempotent-Replayed | 同一个幂等键再次请求、命中已存结果并被重放时。 | 值为 true 表示没有产生新的执行、也没有二次扣费——把响应体当作首次请求的结果即可。 |
| X-Idempotency-Sealed | 原始响应无法安全重放(例如流式响应或响应体过大)。 | 不要再用这个键重试。改为通过任务查询接口或你自己的记录来对账。 |
const response = await fetch(url, init);
const requestId = response.headers.get("X-Oneapi-Request-Id");
if (!response.ok) {
// Bodies are absent on 429 and 413 — never assume JSON here.
const detail = await response.text();
logger.error({ status: response.status, requestId, detail });
throw new GatewayError(response.status, requestId, detail);
}#错误码表
以下是网关自身抛出的错误,type 恒为 "xiaoxiang_error"。其中两行同为 429 但含义相反——退避之前先读 code。
| HTTP | 错误码 | 含义 | 处理方式 | 可重试? |
|---|---|---|---|---|
| 401 | 无 | Bearer 令牌缺失、格式错误、已删除或已禁用。这一层不写 code,请按状态码判断。 | 检查 Authorization 头,并到控制台 → 密钥 确认密钥状态。 | 否 |
| 401 | signature_required | 这个 key 开启了请求签名,但请求缺少 X-Timestamp、X-Nonce 或 X-Signature。 | 给这个 key 的每个请求签名(认证与密钥 → App 请求签名),或在 key 上关闭签名。 | 否 |
| 401 | signature_expired | X-Timestamp 不是 10 位秒级时间戳,或与服务器时间相差超过 5 分钟。响应会带 X-Server-Timestamp。 | 按 X-Server-Timestamp 的差值校准客户端时间,用新的 nonce 重新签名。 | 是 |
| 401 | signature_invalid | X-Signature 与这个 key 的签名密钥对不上,或 X-Nonce 格式不对。 | 用控制台 → API 密钥 里当前的签名密钥,对时间戳、nonce、方法、路径(不含查询参数)以换行连接后的内容签名。 | 否 |
| 401 | signature_replayed | 这个 X-Nonce 在 10 分钟内已经被这个 key 用过。 | 每个请求(包括重试)都生成新的随机 nonce。 | 是 |
| 403 | access_denied | 调用方 IP 不在该密钥的 IP 白名单内。 | 在控制台 → 密钥中加入出口地址;若没有固定出口 IP,就取消该密钥的白名单。 | 否 |
| 403 | 无 | 账号被封禁,或该密钥无权使用这个模型 / 渠道。 | 先检查密钥的模型白名单;如果没有限制,请提交工单——问题出在账号本身。 | 否 |
| 400 | unsupported_endpoint | 模型存在,但不服务这个路径。错误消息里会列出它支持的端点。 | 改调用列出的端点——图片任务型模型不会响应 /v1/chat/completions。 | 否 |
| 400 | read_request_body_failed | 请求体读取失败——上传被截断、Content-Type 不匹配,或流提前结束。 | 发送完整的请求体并带上匹配的 Content-Type。原样重发只会以同样方式失败。 | 否 |
| 402 | insufficient_user_quota | 钱包余额不足以覆盖本次调用的预扣额度。 | 在控制台 → 钱包充值后重试。 | 否 |
| 429 | insufficient_user_quota | 账号已触发每日消费上限。状态码和限流相同,含义却相反——退避重试不会让它恢复。 | 在账号设置里调高或取消上限,或次日再继续。请按 code 分支处理,不要只看 429。 | 否 |
| 429 | 无 | 密钥级 RPM/RPD 限流或并发保护。响应体为空,等待时长在 Retry-After 里。 | 先按 Retry-After 秒数等待,再以指数退避加随机抖动重试。 | 是 |
| 413 | read_request_body_failed | 请求体超过允许大小。传输层限制会直接中断,且不返回响应体。 | 缩小请求体——通过 /v1/media/upload-policy 上传文件并传 URL,不要用 Base64。 | 否 |
| 422 | invalid_request | 没有渠道支持该模型的这种媒体意图——例如在只支持文生视频的路径上发图生视频。 | 查看模型卡确认支持的意图,然后更换模型或意图。 | 否 |
| 503 | model_not_found | 模型名不存在,或当前没有健康渠道可以服务它。注意状态码:模型名写错返回的是 503,不是 404。 | 用 /v1/models 核对名称后重试——故障转移通常在下一次尝试就能找到健康渠道。 | 是 |
| 500 | sensitive_words_detected | 提示词在离开网关之前就被内容过滤拦截了。 | 修改提示词。同样的文本重试永远失败。 | 否 |
| 500 | count_token_failed | 网关无法估算计费所需的 token 数,通常是 messages 数组结构有问题。 | 先校验 messages 数组;结构没问题的话,重试并带上 request id 提工单。 | 是 |
| 500 | invalid_request | 请求无法被解析成该端点接受的结构。注意状态码——网关读不懂的请求体会报 500,而不是 400。 | 对照端点文档校验 JSON 后再重试。 | 否 |
| 502 | do_request_failed | 厂商不可达,或返回了网关无法通过故障转移恢复的错误。 | 退避后重试——下一次尝试通常会落到另一个渠道。 | 是 |
| 504 | do_request_failed | 厂商未在网关超时时间内返回。 | 带退避重试;长 prompt 和繁忙厂商更容易超时。 | 是 |
| 499 | do_request_failed | 你的客户端在结果返回前断开了连接——通常是自己的 HTTP 超时在生成中途触发。 | 调大客户端读取超时。长生成需要几分钟,多数 HTTP 库的默认值远远不够。 | 否 |
幂等守卫错误
这些来自中继之前的重放守卫,因此用的是扁平的 {code, error} 结构、code 为大写,而不是 OpenAI 那套错误结构。
| HTTP | 错误码 | 含义 | 处理方式 | 可重试? |
|---|---|---|---|---|
| 400 | IDEMPOTENCY_STREAM_UNSUPPORTED | 在流式请求上带了 Idempotency-Key。 | 流式调用请去掉该请求头;需要重放保护时改用 stream:false。 | 否 |
| 400 | IDEMPOTENCY_BODY_UNREADABLE | 请求体无法计算幂等账本所需的哈希。 | 请发送常规的 JSON 或 multipart 请求体——无法缓冲的分块请求体会被拒绝。 | 否 |
| 409 | IDEMPOTENCY_PAYLOAD_CONFLICT | 这个键已经用于另一个请求体、路径或查询参数。 | 换一个新键。一个键标识的是一个确定的请求,而不是一轮重试。 | 否 |
| 409 | IDEMPOTENCY_IN_PROGRESS | 使用该键的首次请求仍在执行。 | 按 Retry-After(2 秒)等待后用同一个键重试——你会拿到首次请求的响应。 | 是 |
| 409 | IDEMPOTENCY_RESULT_UNREPLAYABLE | 首次请求已经完成,但响应被封存、无法重放。 | 不要盲目重新提交。先通过任务接口或自己的记录对账。 | 否 |
| 503 | IDEMPOTENCY_UNAVAILABLE | 幂等账本不可用,守卫选择失败关闭,以免造成重复扣费。 | 退避后重试——这次请求并未被执行。 | 是 |
异步任务错误
视频、图片任务、音乐和 3D 端点在提交、轮询、取消时都返回自己的扁平结构 {code, message, data}。有一个需要预留处理的怪异之处:轮询未知任务 ID 是 400,取消未知任务 ID 是 404。
| HTTP | 错误码 | 含义 | 处理方式 | 可重试? |
|---|---|---|---|---|
| 400 | task_not_exist | 轮询或 remix 了一个不存在、或属于其他账号的任务 ID。注意状态码——轮询未知任务是 400,取消未知任务却是 404。 | 提交时就把 task_id 落库,并严格用这个值去轮询。 | 否 |
| 400 | invalid_request | 缺少必填字段——remix 缺 prompt,查询缺 task_id。 | 先修正请求体或路径再重试。 | 否 |
| 400 | task_channel_disable | 衍生任务(remix、续写)会被锁定到原任务的渠道,而该渠道现在已被禁用。 | 改为提交一个全新任务,不要从旧任务派生。 | 否 |
| 400 | invalid_source_task | 源任务类型不匹配——例如把视频任务 ID 传给了 3D 续作接口。 | 确认源任务 ID 来自同一类能力。 | 否 |
| 404 | task_not_exist | 要取消的任务 ID 不存在,或不属于你。 | 没有可取消的对象——按已经不存在处理即可。 | 否 |
| 409 | task_not_cancellable | 任务已进入终态,没有可取消的内容。 | 再轮询一次,直接使用你已经付费的结果。 | 否 |
| 501 | task_cancel_not_supported | 该任务背后的厂商没有提供取消接口。大多数厂商都没有——能取消是例外,不是常态。 | 让任务跑完。界面上要把「取消」设计成尽力而为,而不是一种保证。 | 否 |
| 502 | cancel_task_failed | 取消过程中厂商不可达,任务状态未发生变化。 | 重试一次,仍不行就让它自然跑完。 | 是 |
还是没解决?
把请求 ID 或报错信息发给我们,我们来查。