OpenRouter 可用性配置越加越脆:uptime optimization 页那套组合怎么排查
有一类故障的表现很别扭:平时好好的,一到上游抖动,整批请求就直接失败,而不是像预期那样悄悄换一家继续跑。翻代码会发现,这些服务往往都做过「可用性优化」——供应商顺序钉死、数据策略收紧、量化级别限定,每一条单看都在提高确定性,合在一起却把自动恢复的空间挤没了。
OpenRouter 官方文档《Uptime Optimization》(openrouter.ai/docs/guides/best-practices/uptime-optimization)这一页很短,但它把因果关系说得很直白:平台会持续跟踪各家供应商的响应时间、错误率与可用性,并据此做路由决策;而当上游出错时,能不能切到另一家健康的供应商,取决于你的请求过滤条件是否允许(该页原文写的是 “if your request filters allow it”)。这句话就是本文要排查的东西——恢复能力不是平台单方面给的,是你和平台共同决定的。
先说明一点:这一页嵌了运行时组件,图表数据是页面打开时才去拉的,文档文本里只有组件代码。所以本文不出现任何「哪家供应商可用性如何」的名单与数值,只讲机制与字段语义。
一、先把「默认可用性」的口径摆正
很多人误以为不配 provider 就是「随便挑一家」。官方文档《Provider Routing》(openrouter.ai/docs/guides/routing/provider-selection)写明的默认策略是有结构的,共三步:先把近期出现过明显故障的供应商排在后面;在稳定的候选里按价格挑,且被选中的概率与价格的平方成反比;剩下的供应商留作 fallback。
这里有一条最容易被忽略的联动,文档里只有一句话:只要你设置了 sort 或 order,负载均衡就会被关闭。 你为了「稳一点」把 order 写死的那一刻,默认那套按健康度加权的负载均衡就不再生效,路由改为按你给的顺序一个个试。这是文档写明的行为,但它和不少人的直觉正好相反。
另一层是跨模型的兜底。文档《Model Fallbacks》(openrouter.ai/docs/guides/routing/model-fallbacks)写明 models 参数按优先级顺序接受一个模型数组,首选返回错误时自动试下一个;默认情况下任何错误都可以触发 fallback,文档列出的包括上下文长度校验错误、内容审核标记、限流与宕机。这是与 provider 那一层平行的另一道保险,两者别混为一谈。
二、判定动作:把路由决策打开看
猜没有意义,OpenRouter 提供了一个可执行的判定动作。官方文档《Router Metadata》(openrouter.ai/docs/guides/features/router-metadata)写明,加一个请求头就能让响应带上 openrouter_metadata:
curl https://openrouter.ai/api/v1/chat/completions \
-H "Authorization: Bearer <OPENROUTER_API_KEY>" \
-H "Content-Type: application/json" \
-H "X-OpenRouter-Metadata: enabled" \
-d '{
"model": "openai/gpt-4o-mini",
"messages": [{ "role": "user", "content": "Hello" }]
}'
(示例里的模型 slug 是官方文档当时写的示例值,平台上有哪些模型随时在变,别当清单用。)
这个头只接受 enabled 与 disabled 两个值,大小写不敏感;文档写明任何其它值(包括拼错、空串)都会退回 disabled,不带这个头时默认也是 disabled。
拿到响应后,看这几个字段就够定位了:
| 字段 | 文档写明的含义 | 排查时怎么读 |
|---|---|---|
endpoints | 文档写明是「本次考虑过的候选端点快照,以及哪一个被选中」 | 整体判断候选集有没有被自己筛窄 |
endpoints.total / endpoints.available | 这两个子字段文档只在错误与成功响应的示例报文里给出,没有单独的字段说明表 | total 很小、available 里没有别的候选,就是筛窄了 |
attempt | 从 1 开始计数的成功尝试序号 | 大于 1 说明前面的尝试失败并已 fallback |
attempts | 可选字段,逐次尝试的供应商/模型/状态码 | 重试确实发生过的直接证据 |
strategy | 本次使用的路由策略 | 与你以为自己在用的那种对不对得上 |
文档给的错误响应示例里,「没有可用供应商」的那一档是 404,报文里会直接点名冲突:某模型是由哪家在服务,而你的 provider.only 只允许了另一家;同时 openrouter_metadata 里的 attempt 是 0。看到 attempt: 0,基本可以判定请求根本没走到任何供应商,问题出在过滤条件而不是上游。 另外,错误响应里的 openrouter_metadata 是挂在错误信封的顶层、与 error 平级,不是嵌在 error 里面,取字段时别摸错位置。
两个坑要提前知道,否则你会以为「打开了但没生效」:文档写明缓存命中的响应永远不带 openrouter_metadata,流式与非流式都会剥掉;另外请求以 500 失败时,message 会被换成通用字符串,provider_code 与 openrouter_metadata 一并省略,只留下 error_type(值为 server)。这条只针对 500:文档写明 502、503、504、529 这些 5xx 在客户端已经 opt-in 的情况下仍然带着 metadata,所以别把「500 看不到」推广成「5xx 都看不到」。非 500 的错误还会在 error.metadata.provider_code 里带上游自己的错误码(有的话)。
还有一处两页之间的口径差异值得留意:Router Metadata 那一页写明旧的头名 X-OpenRouter-Experimental-Metadata 出于向后兼容仍被接受,并建议迁移到 X-OpenRouter-Metadata;而《Errors and Debugging》那一页的示例里用的仍是旧头名。两处都是官方文档,我们不替它判断哪个更新——按新头名写、旧头名当兼容项知道就行,具体以官方文档最新内容为准。
Windows 侧补一句(这属于通用的命令行差异,不是 OpenRouter 官方文档内容):上面这段是 Unix shell 写法,反斜杠续行、单引号包 JSON 在 PowerShell 里都不成立——PowerShell 里 curl 默认是 Invoke-WebRequest 的别名,要用真的 curl 得写 curl.exe,续行符是反引号。排查阶段更省事的做法是直接用官方文档里那份 Python 示例,跨平台行为一致。
三、按「收窄程度」重排你的可用性配置
确认是过滤条件太紧之后,处置的思路不是逐个删参数,而是把这些字段按它们对候选集做的事分层,再决定哪一层必须保留。下面这张表只摘了与可用性直接相关的几行,完整字段表在官方文档那一页。
| 字段 | 文档写明的语义 | 对候选集的影响 |
|---|---|---|
order | 按给定顺序优先尝试供应商 slug | 只改顺序,未列出的仍可兜底 |
allow_fallbacks | 主选不可用时是否允许备用供应商,默认 true | 置 false 后只剩你列的那些 |
only / ignore | 本次请求允许 / 跳过的供应商 slug | 硬白名单与硬黑名单 |
require_parameters | 只用支持请求中全部参数的供应商,默认 false | 置 true 后不支持的直接不路由 |
data_collection | allow(默认)/ deny,控制是否使用可能存储数据的供应商 | deny 只留不收集用户数据的 |
zdr | 只路由到 ZDR(Zero Data Retention)端点 | 合规收窄 |
quantizations | 按量化级别过滤供应商 | 文档写明供应商是为开放权重模型提供不同量化级别的,所以这条只在这类模型上起作用 |
preferred_min_throughput / preferred_max_latency | 偏好的吞吐下限与延迟上限 | 不达标的被移到列表末尾而非排除 |
(以上默认值是官方文档写明的默认值,随版本可能变动。)
最后一行值得单独强调:文档明确写了性能阈值是降优先级而不是剔除——不达标的端点被移到列表末尾。所以性能阈值不是候选集变空的元凶,only、allow_fallbacks: false、data_collection: "deny"、zdr 这几个才是硬筛。这也是排查时的分工:合规类的硬筛通常不能动,那就把可用性的冗余补在别处。
require_parameters 还有个细节:文档写明即使它是 false,tools、response_format(含结构化输出)与 verbosity 这一小组参数仍会作为软偏好参与选择——有的供应商支持、有的不支持时只路由到支持的那些;全都不支持时请求照发、参数被忽略,这条偏好不会把某个模型从 models 兜底列表里踢掉。
还有一个和 order、only、ignore 都相关的匹配语义,配错了会静悄悄地把范围放大或缩小:基础 slug 匹配该供应商的全部端点,含各区域与变体;要精确到某个变体或区域,必须写全带后缀的 slug。文档给的对照是 "google-vertex" 匹配它的所有区域端点,而 "google-vertex/us-east5" 只匹配那一个区域;"deepinfra" 匹配默认与 turbo 两种,"deepinfra/turbo" 只匹配后者。文档还特别注明:service tier 端点不被基础 slug 匹配,需要通过 service_tier 参数或带 tier 后缀的 slug 显式选入。这些端点名与供应商名是静态写在官方文档正文里的示例,平台上的供应商与端点随时在变,以官方文档最新内容为准。
一个把两层兜底都留着、同时保留一条合规硬筛的组合大致长这样:
{
"model": "mistralai/mixtral-8x7b-instruct",
"models": ["~anthropic/claude-sonnet-latest", "gryphe/mythomax-l2-13b"],
"messages": [{ "role": "user", "content": "Hello" }],
"provider": {
"order": ["openai", "together"],
"allow_fallbacks": true,
"data_collection": "deny"
}
}
以上为按官方文档中的字段语义组合的示例,未经实测,以官方文档与 API 的实际响应为准。
如果你走的是 Anthropic Messages 端点(/api/v1/messages),对应的参数叫 fallbacks,文档写明它映射到 OpenRouter 的 models 路由,触发条件与上面列的那些错误一致,不是只在拒答时才触发;但它的边界也写得很死:每条只接受 model 字段,fallbacks 与 models 不能同时发送,条目数超过文档写明的上限会返回 400。
四、处置后怎么验证
改完别靠感觉,按顺序做三件事:
第一,仍带 X-OpenRouter-Metadata: enabled 重发同样的请求,比较 endpoints.total 与 endpoints.available 的变化——候选端点确实变多了,才说明改动落到了路由上;如果数字没动,你改的那个字段大概率不在关键路径上。
第二,人为制造一次失败场景观察 attempt 与 attempts。文档写明 attempt 大于 1 就意味着前面的尝试失败并成功 fallback 了,attempts 里能看到每次的供应商、模型与状态码。这是「兜底真的发生过」的直接证据,比看总体成功率靠谱。
第三,把重试逻辑对齐到文档给的信号。官方文档《Errors and Debugging》(openrouter.ai/docs/api_reference/errors-and-debugging)写明,429 与 503 响应上可能带标准的 Retry-After 头,OpenAI SDK、Anthropic SDK、Vercel AI SDK 与 OpenRouter SDK 都已经遵循它,直接用 fetch 的则要自己在重试前尊重它。分类统一看 error.metadata.error_type:provider_overloaded 对应 503,provider_unavailable 对应 502,文档写明后者在启用 fallback 路由时平台可能自动改用另一家重试。文档还说 error_type 是跨几种 API 形态都稳定的字段,原生协议码只是尽力而为,别拿它写判断逻辑。
至于逐家供应商的可用性数据,《Uptime Optimization》那一页写明可以通过 Endpoints API 以程序方式获取,要做长期监控就从这里取。
五、什么情况说明不是这个原因
这一步比前面都重要,省掉它就会有人把所有失败都归到「路由配窄了」,然后把合规配置删得干干净净还是没修好。
看到这些就不是过滤条件的问题:402 是账户或密钥余额不足,401 是凭证无效,403 是权限、guardrail 拦截或审核标记,400 是参数问题——文档把它们和路由分得很清楚,加再多 fallback 也不会变。
流式请求中途报错也不是。 文档写得很明确:一旦第一个 token 已经写给客户端,HTTP 200 与响应头就已经提交、无法再改,此时供应商失败,OpenRouter 无法静默切到另一家,错误只能作为 SSE 事件带内送达。反过来说,如果错误发生在任何 token 写出之前,即便是流式请求也仍可透明地换备用供应商重试。所以「中途断在半截」这类问题要在客户端处理,不该指望调路由参数解决。
「没有生成任何内容」通常也不是。 文档只列了两个典型成因:模型正从冷启动预热、系统正在扩容以承接更多请求;预热耗时随模型与供应商而异,文档只给了「几秒到几分钟」这个量级,没有承诺具体时长。建议的处置是加一个简单的重试机制,或换一个近期活跃度更高的模型/供应商,同时提醒即使没有生成内容,也可能仍被上游收取提示处理费用。
500 时别据此下结论。 前面说过,500 会把 openrouter_metadata 一并省略,你看不到路由上下文,此时「候选集看起来是空的」只是因为信息被遮蔽了,不能作为筛空的证据。
真正指向本文这条原因的,只有一组信号:404 或 503 且报文点名了你的 provider 偏好与实际服务方冲突,或者 openrouter_metadata 里 attempt 为 0、endpoints.total 小得不合理。凑不齐这组信号,就该去查别的地方。
本文依据 OpenRouter 官方文档(openrouter.ai/docs)于 2026-08-18 的公开内容整理。
该平台闭源,本文只复述官方文档写明的机制,不推断其内部实现;
我们没有对文中涉及的功能做过实测,因此不涉及界面外观与运行表现的任何描述。
该平台的供应商、模型与路由策略随时变动,文中不列具体供应商名单与模型清单;
价格、额度与限流的具体数值请以官方定价页与用量说明为准。