遇到问题

查报错

#贴上报错

读错误只按一个顺序:先看 HTTP 状态码,再看 error.type,最后看 error.code。状态码永远有意义;type 告诉你是谁拒绝了请求;只有网关自己抛出的错误,code 才是稳定的。

#如何分支处理

  1. 1

    先按状态码分支

    402 是去充值,403 是密钥权限,429 是等待,5xx 是重试。这一层不会骗你,也不会变。

  2. 2

    再看 error.type

    xiaoxiang_error 是我们的。其他值——openai_error、claude_error、gemini_error——都是厂商自己的响应体,原样透传。

  3. 3

    最后才读 error.code

    只对 xiaoxiang_error 按 code 分支。上游的 code 属于厂商,随时可能变。

并不是每个错误都有 JSON 响应体

限流拒绝和请求体过大只返回一个状态行。如果你的解析逻辑默认 response.json() 一定成功,那么在错误分支上它会抛异常,而不是处理错误。
错误结构(四种)

会到达客户端的一共五种结构。五种都要处理——后三种是接入时最常被漏掉的。

网关自身错误

请求被网关自身拒绝——认证、额度、路由或请求结构问题。

http
{
  "error": {
    "message": "insufficient account balance, please top up to continue (request id: 20260910...)",
    "type": "xiaoxiang_error",
    "code": "insufficient_user_quota"
  }
}

上游错误,原样透传

厂商返回了自己的错误。响应体原样转发,因此 type 和 code 属于厂商,不是我们的。

http
{
  "error": {
    "message": "This model's maximum context length is 128000 tokens.",
    "type": "invalid_request_error",
    "code": "context_length_exceeded"
  }
}

前置守卫拒绝

幂等等少数前置守卫在进入中继之前就返回了,此时还没有中继错误结构:扁平结构、大写 code。

http
{
  "code": "IDEMPOTENCY_IN_PROGRESS",
  "error": "a request with this Idempotency-Key is still processing"
}

异步任务拒绝

所有任务端点——视频、图片任务、音乐、3D——都用自己的扁平结构报告失败。这里没有嵌套的 error 对象,因此按 /v1/chat/completions 写的解析逻辑在这里会读到 null。

http
{
  "code": "task_not_exist",
  "message": "task does not exist",
  "data": null
}

只有状态码,响应体为空

限流拒绝和请求体过大只返回一个状态行,没有响应体。

http
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原始响应无法安全重放(例如流式响应或响应体过大)。不要再用这个键重试。改为通过任务查询接口或你自己的记录来对账。
log-the-request-id.ts
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 头,并到控制台 → 密钥 确认密钥状态。否
401signature_required这个 key 开启了请求签名,但请求缺少 X-Timestamp、X-Nonce 或 X-Signature。给这个 key 的每个请求签名(认证与密钥 → App 请求签名),或在 key 上关闭签名。否
401signature_expiredX-Timestamp 不是 10 位秒级时间戳,或与服务器时间相差超过 5 分钟。响应会带 X-Server-Timestamp。按 X-Server-Timestamp 的差值校准客户端时间,用新的 nonce 重新签名。是
401signature_invalidX-Signature 与这个 key 的签名密钥对不上,或 X-Nonce 格式不对。用控制台 → API 密钥 里当前的签名密钥,对时间戳、nonce、方法、路径(不含查询参数)以换行连接后的内容签名。否
401signature_replayed这个 X-Nonce 在 10 分钟内已经被这个 key 用过。每个请求(包括重试)都生成新的随机 nonce。是
403access_denied调用方 IP 不在该密钥的 IP 白名单内。在控制台 → 密钥中加入出口地址;若没有固定出口 IP,就取消该密钥的白名单。否
403无账号被封禁,或该密钥无权使用这个模型 / 渠道。先检查密钥的模型白名单;如果没有限制,请提交工单——问题出在账号本身。否
400unsupported_endpoint模型存在,但不服务这个路径。错误消息里会列出它支持的端点。改调用列出的端点——图片任务型模型不会响应 /v1/chat/completions。否
400read_request_body_failed请求体读取失败——上传被截断、Content-Type 不匹配,或流提前结束。发送完整的请求体并带上匹配的 Content-Type。原样重发只会以同样方式失败。否
402insufficient_user_quota钱包余额不足以覆盖本次调用的预扣额度。在控制台 → 钱包充值后重试。否
429insufficient_user_quota账号已触发每日消费上限。状态码和限流相同,含义却相反——退避重试不会让它恢复。在账号设置里调高或取消上限,或次日再继续。请按 code 分支处理,不要只看 429。否
429无密钥级 RPM/RPD 限流或并发保护。响应体为空,等待时长在 Retry-After 里。先按 Retry-After 秒数等待,再以指数退避加随机抖动重试。是
413read_request_body_failed请求体超过允许大小。传输层限制会直接中断,且不返回响应体。缩小请求体——通过 /v1/media/upload-policy 上传文件并传 URL,不要用 Base64。否
422invalid_request没有渠道支持该模型的这种媒体意图——例如在只支持文生视频的路径上发图生视频。查看模型卡确认支持的意图,然后更换模型或意图。否
503model_not_found模型名不存在,或当前没有健康渠道可以服务它。注意状态码:模型名写错返回的是 503,不是 404。用 /v1/models 核对名称后重试——故障转移通常在下一次尝试就能找到健康渠道。是
500sensitive_words_detected提示词在离开网关之前就被内容过滤拦截了。修改提示词。同样的文本重试永远失败。否
500count_token_failed网关无法估算计费所需的 token 数,通常是 messages 数组结构有问题。先校验 messages 数组;结构没问题的话,重试并带上 request id 提工单。是
500invalid_request请求无法被解析成该端点接受的结构。注意状态码——网关读不懂的请求体会报 500,而不是 400。对照端点文档校验 JSON 后再重试。否
502do_request_failed厂商不可达,或返回了网关无法通过故障转移恢复的错误。退避后重试——下一次尝试通常会落到另一个渠道。是
504do_request_failed厂商未在网关超时时间内返回。带退避重试;长 prompt 和繁忙厂商更容易超时。是
499do_request_failed你的客户端在结果返回前断开了连接——通常是自己的 HTTP 超时在生成中途触发。调大客户端读取超时。长生成需要几分钟,多数 HTTP 库的默认值远远不够。否
幂等守卫错误

这些来自中继之前的重放守卫,因此用的是扁平的 {code, error} 结构、code 为大写,而不是 OpenAI 那套错误结构。

HTTP错误码含义处理方式可重试?
400IDEMPOTENCY_STREAM_UNSUPPORTED在流式请求上带了 Idempotency-Key。流式调用请去掉该请求头;需要重放保护时改用 stream:false。否
400IDEMPOTENCY_BODY_UNREADABLE请求体无法计算幂等账本所需的哈希。请发送常规的 JSON 或 multipart 请求体——无法缓冲的分块请求体会被拒绝。否
409IDEMPOTENCY_PAYLOAD_CONFLICT这个键已经用于另一个请求体、路径或查询参数。换一个新键。一个键标识的是一个确定的请求,而不是一轮重试。否
409IDEMPOTENCY_IN_PROGRESS使用该键的首次请求仍在执行。按 Retry-After(2 秒)等待后用同一个键重试——你会拿到首次请求的响应。是
409IDEMPOTENCY_RESULT_UNREPLAYABLE首次请求已经完成,但响应被封存、无法重放。不要盲目重新提交。先通过任务接口或自己的记录对账。否
503IDEMPOTENCY_UNAVAILABLE幂等账本不可用,守卫选择失败关闭,以免造成重复扣费。退避后重试——这次请求并未被执行。是

幂等键的工作方式 →

异步任务错误

视频、图片任务、音乐和 3D 端点在提交、轮询、取消时都返回自己的扁平结构 {code, message, data}。有一个需要预留处理的怪异之处:轮询未知任务 ID 是 400,取消未知任务 ID 是 404。

HTTP错误码含义处理方式可重试?
400task_not_exist轮询或 remix 了一个不存在、或属于其他账号的任务 ID。注意状态码——轮询未知任务是 400,取消未知任务却是 404。提交时就把 task_id 落库,并严格用这个值去轮询。否
400invalid_request缺少必填字段——remix 缺 prompt,查询缺 task_id。先修正请求体或路径再重试。否
400task_channel_disable衍生任务(remix、续写)会被锁定到原任务的渠道,而该渠道现在已被禁用。改为提交一个全新任务,不要从旧任务派生。否
400invalid_source_task源任务类型不匹配——例如把视频任务 ID 传给了 3D 续作接口。确认源任务 ID 来自同一类能力。否
404task_not_exist要取消的任务 ID 不存在,或不属于你。没有可取消的对象——按已经不存在处理即可。否
409task_not_cancellable任务已进入终态,没有可取消的内容。再轮询一次,直接使用你已经付费的结果。否
501task_cancel_not_supported该任务背后的厂商没有提供取消接口。大多数厂商都没有——能取消是例外,不是常态。让任务跑完。界面上要把「取消」设计成尽力而为,而不是一种保证。否
502cancel_task_failed取消过程中厂商不可达,任务状态未发生变化。重试一次,仍不行就让它自然跑完。是

异步任务生命周期 →

还是没解决?

把请求 ID 或报错信息发给我们,我们来查。