OpenRouter 请求超时怎么排查:超时到底发生在哪一段

2026-08-31
站内工具 AI 编程工具报错分诊器 → 把报错原文贴进去,先分清是网络、额度、配置还是上游故障,再决定往哪个方向查。

数据截至 2026-08,价格与限额以各官网为准。本文只讲计费与接入机制,不列具体价格数字。

先把结论摆出来:在 OpenRouter 这条链上,「超时」至少有三段,而它们的处理方式完全不同。第一段是你自己的客户端超时(SDK 的 timeout 参数、你手写 fetch 时挂的 AbortSignal);第二段是 OpenRouter 到上游供应商之间的 fetch 超时——官方给供应商的接入文档里明说,如果供应商处理耗时又不发 SSE keep-alive,OpenRouter 可能会用 fetch 超时取消掉这次调用并回退到另一个供应商;第三段是供应商自己的读超时,也就是模型在生成途中不再吐字、读取截止时间到了。官方规范化后的 error_type 里有一个取值就叫 timeout,它的定义是「供应商没有在允许的时间内响应」——注意这句话的主语是供应商,不是你,也不是 OpenRouter 网关。 更要命的一点是:客户端这边超时了,不代表账单那边也停了。下面按排查顺序走一遍。

先看官方怎么给延迟分段

OpenRouter 在讲延迟优化的那部分文档里,直接给了一个把总延迟拆开的式子:

Total Latency = TTFT (Network + Queue + Prefill) + (Output Tokens / Generation TPS)

对应的解释也写得很清楚:TTFT(首 token 时间)由网络传输、供应商的排队等待、以及 prompt 的 prefill 三部分构成;后半段则是 token 吞吐(TPS)决定的解码与流式输出速度。

这个式子对排查的价值在于,它把「慢」分成了两类完全不同的病:

  • 迟迟不出第一个 token:问题在网络、在供应商队列、或者在你 prompt 太长导致 prefill 久。官方在讲峰值场景时提到,热门开源权重模型的托管方在流量高峰会出现队列拥塞,表现就是 TTFT 抬升。
  • 第一个 token 出来了但后面拖很久:问题在输出长度与生成吞吐。输出 token 数是你自己控制的变量,吞吐是供应商侧的。

所以你遇到超时的第一个动作不该是加大超时阈值,而是先确认它卡在哪一段——有没有拿到过任何一个 chunk,是判断这件事最省事的信号。

官方把超时标成什么:一个 error_type,三种皮肤

OpenRouter 会把所有上游供应商的错误归一成一套稳定的 error_type 词表。官方明确建议:用这个字段而不是只看 HTTP 状态码来区分错误类别,因为各家原生协议码(Anthropic 的 error.type、Responses 的 error.code)是尽力而为的,在不同格式之间会不一致。

跟超时相关的取值在「Generic」那一组里:

error_type官方描述
timeout供应商未在允许的时间内响应
server意外的内部错误,上游错误信息在这个类型上会被屏蔽
unmapped无法归入任何已知类别的上游错误,原始码可能在 error.metadata.provider_code

取值以官方文档为准。这里有个坑必须提前说:timeout 在不同 API 皮肤上呈现出来的样子不一样

  • Chat Completions/api/v1/chat/completions):读 error.metadata.error_type
  • Anthropic Messages/api/v1/messages):内部的 timeout 会映射成 Anthropic 原生的 timeout_error,同时官方会把规范化的 error_type 也塞进 error 对象里一起返回。
  • Responses/api/v1/responses):这一个最容易误判。官方给的映射表里,authenticationprovider_overloadedprovider_unavailabletimeoutserver 这五个内部类型统统塌成同一个原生码 server_error。也就是说你在 Responses 上看到 server_error,光凭它根本分不出是超时还是鉴权挂了。官方的补救是把规范化的 error_type 放在响应对象的顶层字段上,不在原生 error 对象里面——所以必须去顶层读。

至于 HTTP 状态码,官方错误码清单里有一条就是「你的请求超时了」;各语言 SDK 生成的错误类对照表里,可以看到超时对应的类是 RequestTimeoutResponseError(408),另有一个 EdgeNetworkTimeoutResponseError 对应 524。它们和 502、503、529 一样属于需要区别对待的一族,别一股脑当成 500 处理——关于 500 的特殊性,另有一篇讲得更细:OpenRouter 报错 500 是谁的问题

第一个 token 是分界线,前后是两个世界

这是 OpenRouter 流式设计里最值得记住的一条规则,也直接决定了超时之后你还有没有救。

出第一个 token 之前,HTTP 响应还没提交,OpenRouter 手里的牌很多:可以返回一个正经的 4xx/5xx 状态码,可以在开启了 fallback 路由的情况下静默地换一个供应商端点重试,也可以在任何实际工作开始前先把限流和鉴权检查跑掉。官方特别加了一条说明:即便是流式请求,只要一个 token 都还没写出去,OpenRouter 仍然能透明地换备用供应商重试。你会在这个阶段看到的典型错误是 key 无效、请求格式非法,或者所有可用端点在开始流式输出前就被耗尽了。

第一个 token 写出去之后200 OK 和响应头已经提交、改不了了。此时供应商挂掉,OpenRouter 无法再静默故障转移,因为部分内容已经交付给你的应用了。错误只能以带内的 SSE 事件形式送达。官方列的中途失败常见原因里,有两条正是超时相关:

  • Provider disconnect——上游连接在输出到一半时断开(网络问题、供应商崩溃、负载均衡器超时)
  • Provider timeout——模型在生成途中不再响应,读取截止时间到了

这类中途错误会以一个 chat.completion.chunk 的形式发过来,顶层带 error 对象,choices 里带一个 finish_reason: "error" 用来正常终止流,HTTP 状态仍然是 200,事件发完流就结束。如果你的代码只判断 HTTP 状态码,这类超时会被你当成成功。

顺带一个手写 SSE 解析必栽的坑:OpenRouter 会在流里穿插发送注释行来防止连接超时,长这样:

: OPENROUTER PROCESSING

按 SSE 规范这类注释可以安全忽略,但官方专门给了警告——如果你自己解析流,必须跳过以 : 开头的行再调 JSON.parse,把这行喂给 JSON.parse 会抛异常,没接住就会把整个流循环搞崩。想拿它做点好事的话,可以用它驱动一个动态加载指示器。

你的客户端超时了,模型不一定停

这一段是本文最贵的信息,因为它跟钱直接相关。

官方在流式取消那一节写得很明确:流式请求可以通过中止连接来取消,对于支持的供应商,这会立即停止模型处理和计费。文档里列了一份支持与不支持取消的供应商名单(名单以官方文档为准,会变)。紧跟着是一条警告:取消只对流式请求且供应商支持的情况有效;对于非流式请求或不支持取消的供应商,模型会继续处理,并且你会为完整的响应付费。

把这句话和「客户端超时」放在一起就明白了:你在客户端 timeout 到点、AbortSignal 一发,你这边确实断了,但如果这是个非流式请求,或者路由到的供应商不在支持取消的名单里,那边照样把整段生成跑完,账单照收。这就是为什么超时重试是所有重试里最烧钱的一种——一次超时可能对应两次完整计费。这一类判断的通用原则,另一篇讲过:什么该重试,什么一重试就烧钱

另一侧的兜底叫零补全保险。官方说明是:当响应满足「零补全 token 且 finish reason 为空/null」,或者「finish reason 为 error」这两个条件之一时,模型的 prompt token、补全 token、推理 token 都不会从你的额度里扣,即使上游供应商就 prompt 处理向 OpenRouter 收了钱。它对所有账户默认开启、无需配置,在 Activity 页上表现为该次请求的 token 费用扣减为零。

但要留意它不覆盖在响应失败之前就已经跑完的辅助服务:联网搜索、文件解析 / PDF OCR、网页抓取这些,可能按实际完成的工作照常计费,并出现在这次请求的用量明细里。所以「超时了所以没花钱」是个危险的想当然,尤其当你的请求挂了插件的时候。

还有一种看起来像超时、其实不是的情况:模型没有生成任何内容。官方给的典型原因是模型正从冷启动预热,或者系统正在扩容以承接更多请求;预热时间通常在数秒到数分钟之间,取决于模型和供应商。这种情况官方的建议是加一个简单的重试机制,或者换一个近期活跃度更高的供应商或模型再试。

事后定位:把这一次调用的记录挖出来

超时最难受的地方是现场没了。OpenRouter 给了两条事后通道。

第一条是 generation 记录。 每个响应都会在 X-Generation-Id 响应头里带回本次生成的 ID,四个端点(chat completions、completions、responses、messages)都有。拿这个 ID 去请求 /api/v1/generation?id=<GENERATION_ID>,可以查到这次调用的统计信息。官方还专门提示:如果一个请求已经越过 API 边缘、但在路由器落定状态之前就失败了,事后就得靠这条路把上下文捞回来。

第二条是 router metadata。 这是个按请求开关的选项,发一个请求头 X-OpenRouter-Metadata: enabled 就会在响应里多出一个 openrouter_metadata 对象,记录路由器到底做了什么。官方写明它的用途之一就是归因延迟。跟排超时最相关的几个字段:

  • attempt:从 1 开始计数、最终成功的那一次尝试的序号。大于 1 意味着前面的尝试失败并发生了回退——如果你觉得某次调用「莫名其妙慢」,这个字段能直接告诉你慢在重试上。
  • attempts:可选数组,路由器向 fallback 重试时,逐次记录 provider、model 和 status。
  • endpoints:本次考虑过的候选端点快照,以及哪一个被选中。
  • pipeline:只记录实际生效过的插件阶段。没起作用的插件不会留痕(比如上下文压缩发现输入本来就装得下,就不会出现这个阶段)。超时排查时它能回答「时间是不是花在 guardrail、联网搜索、文件解析这些附加环节上」。

有几条边界必须知道,否则你会以为功能坏了:

  • 流式响应里,openrouter_metadata 是在 data: [DONE] 之前的最后一个 chunk 上送达(Anthropic Messages 则挂在终止的 message_stop 事件上)。请求超时被你中途掐断,这个字段自然就拿不到。
  • 缓存命中永远不带 openrouter_metadata。官方说这是故意的,避免客户端拿陈旧的路由数据下判断。
  • 500 状态的响应会被清洗成通用信息,metadata 也一并省略;但 502、503、504、529 这几类只要你开了开关,路由快照照样返回。
  • 鉴权失败、限流失败这类在路由器拿到可用状态之前就触发的错误,不会带这个字段。
  • 请求头的取值只认 enableddisabled(大小写不敏感),其他任何值——包括拼错、空串、自造的等级名——一律回退成 disabled。这一条很坑,写错了不会报错,只会静悄悄什么都不返回。旧的头名 X-OpenRouter-Experimental-Metadata 仍然被接受,但官方建议迁到新名字。

事前收敛:让慢端点排到后面去

排查完还得降低复发。OpenRouter 在 provider 偏好里给了几个和延迟直接相关的可控项:

preferred_max_latency 接受一个秒数,也可以接受一个带百分位切点的对象(p50、p75、p99 这些百分位口径以官方文档为准)。它评估的是最近 5 分钟的滚动窗口,官方的说法是这样能在某个托管方开始排队时快速适应。

这里有个关键的行为差异必须讲清楚,否则很容易用错:preferred_max_latencypreferred_min_throughput 都不保证你一定会拿到达到该性能水平的供应商或模型,达标的会被优先,但它们永远不会导致你的请求执行不了——达标的端点被提到候选列表前面,如果所有供应商都处于高负载,请求仍然会在当时最好的那个上执行,而不是直接失败。这一点和 max_price 正好相反,后者在价格不可得时会阻止请求运行。

另外几个相关旋钮:sort 可以取 pricethroughputlatency,想始终优先低延迟就把它设成 latency;配 fallback 用多模型时,sort 还能写成对象形式,用 sort.partition 控制是按模型分组排序(默认,主模型的端点永远先试)还是全局排序。allow_fallbacks 默认允许在主供应商不可用时启用备用供应商——这正是「第一个 token 之前」那段能救回来的前提。

供应商侧还有一层你看不见但一直在起作用的机制:官方按可用率给端点分档,正常的走常规路由,降级的优先级下调,判定为下线的只作为兜底使用,而且要积累到一定请求量之后才开始计算可用率(具体比例与请求量门槛以官方文档为准)。值得注意的是哪些错误算进可用率:402、404、所有 500 以上的服务端错误、中途流式错误,以及带 error finish reason 的「成功」请求都算;而 400、413 这类用户输入错误不算,429 单独统计,地域限制的 403 也单独统计。

最容易栽的三个坑

  1. 把客户端超时当成「这次没花钱」。非流式请求,或者路由到的供应商不支持取消,模型会跑完并全额计费。想让取消真的省钱,前提是流式 + 供应商在支持名单内。
  2. 在 Responses API 上看着原生码排错timeout 在那儿塌成 server_error,跟鉴权失败长得一模一样,必须去顶层读 error_type
  3. 超时后无脑加大阈值、无脑重试。先用有没有拿到第一个 chunk 判断卡在哪一段:卡在 TTFT 就往 preferred_max_latency 和 prompt 长度上想,卡在生成中段就往输出长度和 fallback 上想。重试策略本身怎么写才不放大故障,见API 重试和退避怎么写;如果错误体里返回的是 rate_limit_exceeded 而不是 timeout,那是另一条路径,见 OpenRouter 429 怎么办

最后提醒一句:本文所有机制都来自 OpenRouter 官方文档的描述,参数默认值与供应商支持名单都会变,接入前以官方文档当前版本为准。文档里没有给出网关侧超时的具体秒数,本文也不猜——需要确切数值的场景,只能以官方文档或工单答复为准。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。

留言讨论

评论发布后会被人工复核,违规内容将被删除。

    还没有人评论,来说说你的看法

    如果发表没有反应,可以前往联系我们告诉我们。

    OpenRouter 充值不方便?

    国内直连的 OpenAI 兼容端点,一期提供 DeepSeek,注册送 ¥5。

    看替代方案

    这个页面有问题?

    提交时会附带当前页面地址和浏览器信息,帮助我们定位问题。不填联系方式即为匿名。