OpenRouter 连接失败的排查清单:从网络到鉴权逐层定位
数据截至 2026-08,价格与限额以各官网为准。本文只讲计费与接入机制,不列具体价格数字。
「连接失败」在 OpenRouter 上极少真的是网线问题。官方文档把失败明确切成了几段:请求还没送到模型之前的失败会带一个正常的 HTTP 错误状态码和一个结构固定的 JSON 错误体;一旦第一个 token 已经写给客户端,HTTP 状态就已经是 200 且不可更改,后续的失败只能以 SSE 事件的形式塞回流里。所以排查的第一步不是 ping,而是问自己一句:我到底有没有拿到那个 JSON 错误体?拿到了,就说明网络与 TLS 全通,问题在鉴权、余额或路由;没拿到,才轮到传输层。再往后,把判断依据从 HTTP 状态码换成官方那套 error_type 字段,几乎所有「连接失败」都能在两三步内定位。
先看你手里有没有那个 JSON 错误体
官方在错误与调试一节里给出的错误响应结构是固定的:
type ErrorResponse = {
error: {
code: number;
message: string;
metadata?: Record<string, unknown>;
};
};
关键在紧跟着的那句说明:HTTP 响应的状态码会和 error.code 相同——但这只在两种情况下成立,一是你的原始请求本身非法,二是你的 API key 或账户额度不足。除此之外,只要模型已经开始产出内容,返回的 HTTP 状态就不再是错误码了,错误会改从响应体里或者以 SSE 数据事件的形式发出来。
官方给的 JavaScript 示例里那行注释说得很直白:request.status 只有在模型还没开始处理你的请求时才会是一个错误码。这句话是整篇排查的分水岭。你在客户端捕获到的「连接失败」,只要能打印出 response.error.code 和 response.error.message,就已经排除了 DNS、TLS、代理这一整层——链路是通的,是服务端主动拒绝了你。
反过来,如果你的客户端抛的是底层网络异常,连响应体都没有,那才需要去看本机出口。这一段官方文档没有涉及,也不该由文档来回答,属于你自己环境的问题。
鉴权层:地址写对了,头也要写对
OpenRouter 的鉴权方式是 Bearer token。官方在 Authentication 一节里写明,直接调用 API 时要把 Authorization 头设成 Bearer 加上你的 API key;如果用 OpenAI 的 SDK,则是把 base URL 指向 https://openrouter.ai/api/v1,再把 apiKey 换成 OpenRouter 的 key。除此之外还有两个可选头 HTTP-Referer 和 X-OpenRouter-Title,作用是让你的应用出现在 OpenRouter 的榜单上,不填不影响调用。
鉴权失败在官方错误码清单里的描述是「凭据无效」,并且列出了三种成因:OAuth 会话过期、key 被禁用、key 无效。注意这三种在客户端看来是同一个状态码,但成因完全不同——OAuth 会话过期意味着你走的是授权流程而不是自己建的 key,禁用则是这把 key 还在但已经不能用了。文档另外提醒过一句:OpenRouter 的 API key 比直接找模型厂商拿的 key 权限更大,它可以为应用设置额度上限,也可以参与 OAuth 流程。这意味着一把 key 出问题的面比你想象的宽。具体的分支判断可以看OpenRouter 报错 401 的排查顺序。
还有一个容易被算到「连接失败」头上的情况:官方把 CORS 归在了请求格式错误那一类,和参数缺失、格式非法并列。浏览器里跨域被拦住,控制台经常只显示一句网络错误,但服务端给的其实是一个正常的格式错误响应。
账户层:余额和单 key 上限是两回事
官方 Limits 一节把限制明确分成两种:额度限制管你能花多少,速率限制管你能发多少次请求。额度限制又来自两个地方,一个是账户余额,一个是你可以在单把 API key 上单独设的消费上限。
账户余额这条有个措辞要注意。原文说的是账户余额为负时你可能会看到报错,而且这种报错会波及免费模型;把余额补到零以上就能重新使用这些模型。是「可能」不是「一定」,转述时别把它写成绝对结论。这个设计的实际后果是:你的免费模型请求突然全挂,第一反应往往是「服务不可用」,但真正的原因可能在账单页。
单 key 上限那部分,官方给了可以直接查的字段。对 https://openrouter.ai/api/v1/key 发一个 GET,用同一把 key 做 Bearer 鉴权,返回体里 limit 是这把 key 的额度上限(无上限时为 null),limit_reset 是重置方式,limit_remaining 是还剩多少。同一个响应里还有 label、按全时段与当日/当周/当月切分的 usage 系列字段、include_byok_in_limit,以及一个 is_free_tier 标志表示这个账户是否付过费。文档还特别注明响应里有一个已废弃的 rate_limit 对象,可以放心忽略。
这个接口对排查的价值在于它只返回 key 自身的信息,不需要指定任何模型。文档的原话是,提交一把有效的 API key,你就应该拿到这个形状的响应。所以它天然是一个链路探针:能拿到结构完整的 JSON,说明网络、TLS、鉴权三层全通,剩下的问题只可能在余额、限流或路由。这一步做完,「连接失败」这个笼统的说法就基本可以退休了。
路由层:请求进来了,但没人接
OpenRouter 是一个路由层,它自己不产出内容。官方错误码清单最后两条描述的正是这一层的失败:一条是「你选的模型已下线,或者我们从它那里收到了无效响应」,另一条是「没有任何可用的模型供应商满足你的路由要求」。
后一条尤其容易被误当成网络问题,但它其实是你自己的路由偏好把候选池筛空了。官方在供应商路由文档里给了 allow_fallbacks 这个开关,作用是允许在首选供应商失败时换另一个符合条件的供应商;把它关掉,就是明确要求「只走第一家,失败就失败」。真正会把候选池筛空的是 provider.only——官方那条报错原文自己就点名了它:你只允许了某一家,而这个模型偏偏不是那家在服务。注意 provider.order 不在此列,官方对它的定义是按这个顺序优先尝试,只排序不排除。相关的报错原文与恢复方式见OpenRouter 报 No allowed providers are available 的含义。
上游过载是单独一类。文档里写明 529 对应供应商临时过载,处理办法是把 allow_fallbacks 打开让它自动切到备用供应商。在类型化错误码表里,这一类叫 provider_overloaded,描述是上游临时过载、稍后重试;与之相邻的 provider_unavailable 则是上游返回了无效或空响应,如果开启了 fallback 路由,OpenRouter 可能会自动换一家供应商重试。
还有一种既不是错误也不是断连的情况:模型什么内容都没生成。官方给的解释是模型正在冷启动预热,或者系统正在扩容以应对更多请求,预热时间通常从几秒到几分钟不等。文档建议的做法是加一个简单的重试,或者换一个近期活跃度更高的供应商或模型。这里有一个容易被忽略的后果——文档明确说了,即便没有生成内容,上游供应商仍有可能就 prompt 的处理部分计费。
流式请求里那些「看着像断连」的情况
流式是「连接失败」误报的重灾区,官方把它拆得很细。
第一段是流开始之前。这时 HTTP 响应还没提交,OpenRouter 可以正常返回 4xx/5xx 状态码,可以在开启 fallback 路由时静默换一个供应商端点重试,也可以在做任何实际工作之前先跑完限流与鉴权检查。key 无效、请求格式错、所有可用端点在开流前就被耗尽,都属于这一段。文档特别提醒:这类错误返回的是普通 JSON,不是 SSE,所以客户端要先判断 response.ok,别一上来就当流去解析。
第二段是流开始之后。一旦第一个 token 写给了客户端,HTTP 的 200 和响应头就已经提交、无法更改,此时上游再失败,OpenRouter 就没法静默切换供应商了,因为部分内容已经交付给了你的应用。错误只能以带内的 SSE 事件形式送达。官方列出的常见成因包括:上游连接在部分输出之后断开(网络问题、供应商崩溃、负载均衡器超时)、模型生成到一半不再响应导致读取超时、生成过程中撞到 max_tokens 或上下文窗口被填满、输出内容被审核系统标记、以及上游在开始流式输出后才返回限流或容量错误。
这类错误的结构是一个 chat.completion.chunk,顶层同时带 error 对象和一个 choices 数组,delta.content 为空、finish_reason 为 error,用来正常终结这个流;HTTP 状态仍然是 200,事件发出后流即终止。非流式请求遇到中途失败时,错误则会嵌在最终响应里、和已经产出的部分内容放在一起,finish_reason 同样是 error。
第三段是解析本身。官方文档里有一个非常具体的坑:为了防止连接超时,OpenRouter 会不定期往 SSE 流里发注释行,长这样:
: OPENROUTER PROCESSING
按 SSE 规范这种注释可以安全忽略,你也可以拿它做加载动画。但文档给了一条明确警告:如果你手写流解析,必须先跳过以 : 开头的行再调 JSON.parse,把注释行直接丢给 JSON.parse 会抛异常,没接住就会把你的流循环整个搞崩。表现出来就是流跑到一半程序炸了——看着像连接断了,实际是你自己的解析器。官方推荐的替代方案是用符合规范的解析器,它会帮你处理注释、多行 data: 字段和缓冲。
超时到底发生在哪一段
官方 SDK 参考页为这些接口列出的错误类型与状态码对应关系里,跟超时有关的有两个不同的类:一个是请求超时(408),一个是边缘网络超时(524)。另外还有网关错误(502)、服务不可用(503)和供应商过载(529)。这是一份结构性枚举,以官方文档当前版本为准。
在类型化错误码那套词表里,超时对应的是 timeout,描述是供应商未在允许时间内响应。把这两套对照着看就能分辨:边缘层的超时和「上游模型不吭声」不是一回事。不过要说明的是,官方只给出了 SDK 错误类名与状态码的对应表,并没有解释边缘层超时的具体语义,所以这里只能当作分类枚举来用,别据此推断它在链路的哪一段发生。逐段拆解的写法见OpenRouter 请求超时怎么排查。
限流带来的「卡住」也别归到超时。文档说明,成功的推理响应里不会包含 X-RateLimit-* 头;只有当 OpenRouter 自己因为平台级限制返回错误时,错误响应才会带上 X-RateLimit-Limit、X-RateLimit-Remaining、X-RateLimit-Reset 三个头来描述被撞到的是哪条限制。此外,当所有尝试过的供应商都给出了重试提示时,错误响应还会带 Retry-After。官方明确要求尊重这个头再重试,并提到 OpenAI SDK、Anthropic SDK、Vercel AI SDK 和 OpenRouter SDK 都已经实现了对它的退避处理;直接用 fetch 的话得自己读。
顺带一提一个反直觉的设计:文档说多开账户或多建 key 并不会改变你的速率限制,因为容量是全局管控的;但不同模型的限流是分开的,所以要分摊压力应该换模型而不是换 key。
把 error_type 当判据,而不是 HTTP 状态码
这是官方自己给的建议,也是这份清单最值钱的一条。OpenRouter 会把每一个上游供应商的错误都归一化成一套稳定的 error_type 词表,不管这个错误是出现在非流式的响应体里,还是作为流中途的 SSE 事件送达,用的都是同一套取值。文档明确说:原生协议里的错误码是尽力而为的,在不同格式之间可能不一致,error_type 才是跨所有格式都能依赖的那个字段。
它出现的位置随接口形态而变,这一点要记牢:Chat Completions 在 error.metadata.error_type,Anthropic Messages 在 error.error_type,Responses 则是响应对象的顶层 error_type。Responses 这一路尤其需要它,因为该接口要把内部错误类型映射到 OpenAI 那套更窄的错误码集合上,鉴权失败、上游过载、上游不可用、超时、内部错误会一起塌缩成同一个 server_error,真实原因只保留在顶层的 error_type 里。你要是只看原生 code,这五种完全不同的故障在你眼里长得一模一样。
还有两条掩码规则会直接影响你能看到什么。请求以 500 失败时,message 会被替换成一句通用文案,provider_code 和 openrouter_metadata 都会被省掉,但 error_type 仍然保留,取值是 server。非 500 的错误则相反,上游供应商自己的错误码会在可获得时放到 error.metadata.provider_code 里。所以当你面对一个信息量极少的 500,那不是日志丢了,是官方有意不外泄内部细节——这类问题的归属判断见OpenRouter 报错 500 是谁的问题。词表里还留了一个兜底取值 unmapped,表示上游错误没能映射到任何已知类别,此时原始码可能仍在 provider_code 里。
最后:按这个顺序走一遍
把上面几层压成可执行的顺序,就是:先看有没有 JSON 错误体,没有就查本机出口,有就往下;对 GET /api/v1/key 发一个请求确认鉴权与链路,顺便看 limit_remaining 和账户是否为负;再看是不是路由偏好把候选池筛空了,或者 allow_fallbacks 被关掉了;流式的话,确认你的解析器跳过了 : 开头的注释行,并且区分开流前的普通 JSON 错误和开流后的 SSE 错误事件;最后统一读 error_type 而不是状态码来分类。
最容易栽的坑只有一个:把所有拿不到结果的情况都叫「连接失败」,然后去折腾网络。OpenRouter 的失败模式里,真正属于传输层的那一小部分,恰恰是唯一不会给你 JSON 错误体的那种。剩下的都在告诉你原因,只是需要你去看对字段。
留言讨论
评论发布后会被人工复核,违规内容将被删除。
如果发表没有反应,可以前往联系我们告诉我们。