OpenRouter 的各种限制项:撞上哪一条会返回什么

2026-08-18

线上跑着的服务突然开始返回失败,日志里只有一个状态码和一句英文,这时候最耗时间的不是修,而是判断「到底撞上了哪一条」。OpenRouter 把「限制」这个词分散写在两页文档里,openrouter.ai/docs/api_reference/limits 讲账户和速率,openrouter.ai/docs/api_reference/errors-and-debugging 讲长度、内容策略和上游可用性——它们的错误长得都像失败,但排查动作完全不同。

这篇只做一件事:把限制的维度和它们各自的错误表现对上,让你看到一个响应就能定位到具体哪一类。全文不写任何额度、速率、token 上限的具体数值,那些数字随时在变,写下来半年后就是误导;这里只写有哪几类、怎么识别。

先建立维度清单

limits 这一页开头就写明,OpenRouter 强制执行的是两类限制:

限制类型管什么超出时的错误去哪查
Credit limits你能花多少(账户余额与单个 key 上的额度上限)402GET /api/v1/keylimit_remaining
Rate limits你能发多少请求(免费模型请求上限与 DDoS 防护)429错误响应上的 X-RateLimit-*

但如果你只按这两类去排查,会有相当一部分失败对不上号。errors-and-debugging 页的「Typed error codes」小节里还有几组同样属于「撞上了某个上限」的类型,官方按用途分了组,其中和限制直接相关的是这几类:

  • Token 与长度类context_length_exceeded(输入加输出超过模型上下文窗口)、max_tokens_exceeded(生成时达到 max_tokensmax_completion_tokens)、token_limit_exceeded(文档描述为「OpenRouter 侧强制的 token 预算,例如基于信用额的上限」)、string_too_long(请求里单个字符串字段超过供应商的每字段字符上限)。这一组四条,文档给的 HTTP 状态都是 400
  • 请求体与图片类payload_too_large413)、image_too_large / image_too_small / unsupported_image_format(均 400)。
  • 内容策略类content_policy_violationrefusal(均 400),以及 guardrail 拦截走的 permission_denied403)。
  • 速率与可用性类rate_limit_exceeded429)、provider_overloaded503)、provider_unavailable502)。

这里最该记住的一句是文档自己给的判定原则:error_type 是跨三种 API 形态稳定的字段,原生协议码是尽力而为的、在不同格式之间可能不一致,要在程序里分支就用 error_type,别只看 HTTP 状态码。它出现的位置按形态不同:Chat Completions 在 error.metadata.error_type,Anthropic Messages 在 error.error_type,Responses 在响应对象顶层的 error_type

下面按最常撞的几条走排查。

现象一:402,包括调免费模型时也报

怎么确认。 文档给的确认动作是对 https://openrouter.ai/api/v1/key 发一个 GET。Python 版原样如下:

import requests
import json

response = requests.get(
  url="https://openrouter.ai/api/v1/key",
  headers={
    "Authorization": f"Bearer <OPENROUTER_API_KEY>"
  }
)

print(json.dumps(response.json(), indent=2))

返回体的类型定义里,和额度直接相关的是 limit(该 key 的额度上限,null 表示不限)、limit_reset(重置类型,null 表示从不重置)、limit_remaining(剩余额度,null 表示不限),以及 include_byok_in_limit(外部 BYOK 用量是否计入这个额度)。另外有 usage / usage_daily / usage_weekly / usage_monthly 四个用量字段,byok_usage 系列同理。注意响应里还有一个 rate_limit 对象,文档在类型定义的注释里明确标了它是 deprecated、可以安全忽略——如果你的旧代码还在读它,那是该清理的地方。

Windows 侧和 Linux/macOS 的差别只在密钥怎么进环境变量:PowerShell 里读环境变量写 $env:OPENROUTER_API_KEY,Linux/macOS 的 shell 里写 $OPENROUTER_API_KEY。这一条属于通用 shell 用法,不是 OpenRouter 官方文档的内容,别把它当成产品行为。

文档给的处置。 三条:把余额加到零以上;如果是 key 上的 limit_remaining 用尽,就提高该 key 的额度上限或等它重置(看 limit_reset);以及主动监控——在请求开始失败之前就轮询 GET /api/v1/keylimit_remaining

这里有一个很容易踩的点,文档写得很直白:账户信用余额为负时可能收到 402,连免费模型也会一起挡掉,把余额加回零以上之后这些模型才能重新用。所以「我用的是免费模型,怎么会提示付费」不是矛盾,它就是这么设计的口径。

处置后怎么验证。 再调一次 GET /api/v1/key,确认 limit_remaining 已经不是耗尽状态(null 表示该 key 没设上限)。

什么情况说明不是这个原因。 如果 key 的 limitnull、账户余额也为正,却仍然失败,那就不是额度这一类,去看错误体里的 error_type——payment_required 才归这一类,是 rate_limit_exceeded 就该按下一节走,是 token_limit_exceeded 则虽然文档描述里提到「基于信用额的上限」但它落在 400 而不是 402,排查入口是请求体不是账户。

现象二:429,先分清是平台限的还是上游限的

怎么确认。 文档写明 429 有两个来源:一是 OpenRouter 平台限制(免费模型变体的每分钟/每天请求上限,以及 Cloudflare 的 DDoS 防护),二是服务你这个请求的上游供应商在限流或已满负荷。区分方法是看错误体的 metadata:上游来源时,error.metadata.provider_code 会在可得的情况下带上供应商自己的原始错误码。

免费变体的识别方法文档也给了:模型 ID 以 :free 结尾的就是免费变体。

还有一个头部行为值得单独记:成功的推理响应不带 X-RateLimit-*。只有当 OpenRouter 自己因为平台限制返回 429 时,错误响应上才会带 X-RateLimit-LimitX-RateLimit-RemainingX-RateLimit-Reset 三个头描述被撞上的那条限制;当所有尝试过的供应商都返回了重试提示时,错误响应还会带 Retry-After。这意味着你没法靠读成功响应的响应头来做余量监控,想提前知道还剩多少,文档指的路只有 GET /api/v1/key

顺带一提,文档在这一页顶部的提示里写明:多开账户或多建 API key 不会改变你的速率限制,因为容量是全局管控的;但不同模型有各自不同的限流,所以可以靠分散到不同模型来分担负载。

文档给的处置。 指数退避重试,并在 Retry-After 存在时遵守它;免费变体上可以购买信用额来提高每日上限,或者换成该模型的付费变体(文档写明付费变体没有平台级的请求数上限);如果是供应商侧的限制,就加 fallback 模型,或者放宽 provider 路由偏好,让更多供应商有资格接这个请求。

遵守 Retry-After 的写法,文档给的原样片段是:

const res = await fetch('https://openrouter.ai/api/v1/chat/completions', { ... });
if (res.status === 429 || res.status === 503) {
  const retryAfter = Number(res.headers.get('Retry-After'));
  if (Number.isFinite(retryAfter) && retryAfter > 0) {
    await new Promise((r) => setTimeout(r, retryAfter * 1000));
    // retry the request
  }
}

文档同时说明,OpenAI SDK、Anthropic SDK、Vercel AI SDK 和 OpenRouter SDK 已经会遵守这个头做退避,只有直接用 fetch 时才需要自己处理。

处置后怎么验证。 看错误响应上的 X-RateLimit-ResetRetry-After,按它给的时间点之后再发;不确定余量就回到 GET /api/v1/key

什么情况说明不是这个原因。 如果失败伴随的是 503 而不是 429,文档给 503 的语义是「没有满足你路由要求的可用模型供应商」,建议是放宽供应商偏好或增加 fallback 模型——那是你自己把候选集缩到空了,不是被限流。如果是 502,语义是所选模型下线或返回了无效响应。这两条都不该按限流退避去处理。

现象三:HTTP 是 200,但这次调用其实失败了

这是我认为最值得单独拎出来的一条,因为它会让只统计状态码的监控看不见故障。

怎么确认。 文档写明:一旦首个 token 已经写给客户端,200 OK 状态和响应头就已经提交、改不了了,此时供应商出问题,OpenRouter 无法再静默换一家(因为部分内容已经交付给你的应用),错误只能以带内的 SSE 事件送达。速率限制如果是在流开始之后才撞上的,收到的就不是 HTTP 429,而是一个 finish_reason: "error" 的 SSE 事件。文档给的示例数据是这样:

data: {"id":"cmpl-abc123","object":"chat.completion.chunk","created":1234567890,"model":"openai/gpt-4o","provider":"openai","error":{"code":429,"message":"Rate limit exceeded"},"choices":[{"index":0,"delta":{"content":""},"finish_reason":"error"}]}

(示例里的模型与供应商名是官方文档当时的示例值,平台上有哪些模型与供应商随时在变,别当清单用。)

所以判定动作是:在流式解析里显式检查每个 chunk 的顶层 error 字段和 choices[].finish_reason,不要只在 HTTP 层判成功。 非流式请求下,文档写明的只有一句:当供应商错误中断生成时,非流式响应上同样带 error.metadata.error_type。此时响应体其余部分长什么样、部分内容还在不在,官方文档没有说明这一点,别照着流式那套形状去猜着解析。

文档给的处置。 文档列出的中途出错常见原因包括:供应商断连、供应商超时、生成过程中达到 max_tokens 或上下文窗口被填满、输出侧内容过滤命中、供应商过载。相应地,能在流开始前拦住的,文档明确说是可以被静默重试的——「如果错误发生在任何 token 写出之前,即使是流式请求,OpenRouter 仍然可以透明地换到备用供应商重试」。换句话说,把 fallback 配好只能救前半段,中途断了必须你自己在应用层重发。

处置后怎么验证。 让日志把 error.metadata.error_typefinish_reason 一起记下来,再统计一次成功率——如果之前的成功率是虚高的,改完这一处会立刻掉下来一截,那说明你原来漏计了这类失败。

什么情况说明不是这个原因。 如果错误是带着 4xx/5xx 状态码返回的,那就是 pre-stream 错误,走的是标准错误体那条路,和「已提交 200」无关;文档举的 pre-stream 例子是无效 API key、格式错误的请求,以及流开始前所有可用供应商端点都已耗尽。

现象四:长度类限制在不同 API 形态上表现不一样

同一个「超长」,落到不同的 API 形态上不是同一个样子,这点很反直觉,也在文档里写得很清楚。

Chat Completions 上,context_length_exceeded / max_tokens_exceeded / token_limit_exceeded / string_too_long 这四条都是 400。但在 Responses API(/api/v1/responses)上,文档专门有一张「Error code transformations」表,把这四条转换成成功finish_reason 记为 length,文档给的理由是让基于限制的错误可以被优雅处理、不当作失败对待(这句是文档自述)。

判定动作:如果你从 Chat Completions 迁到 Responses,发现「超长」类错误在监控里消失了,先别高兴,去看返回里的 finish_reason 是不是 length——问题没消失,只是换了个位置表达。反过来说,Responses 上真正失败时,原因也可能被压平:文档的映射表写明 authenticationprovider_overloadedprovider_unavailabletimeoutserver 都会被收敛成原生的 server_error 码,精确原因保留在响应顶层的 error_type 字段里。Anthropic Messages 形态同理,原生 error.type 是有损的(多种内部类型都会落到 api_error),所以文档说规范的 error_type 会额外加在 error 对象里。

什么情况说明不是这个原因。 如果 error_typepayload_too_large413),那不是 token 层面的超长,是请求体字节数超过了允许的最大体积,两者的收敛动作不同:前者要减内容或换上下文更大的模型,后者要减请求体(比如内联的图片数据)。

两处需要你自己核一下的口径

errors-and-debugging 页里,guardrail 那一节写的是把 X-OpenRouter-Experimental-Metadata: enabled 这个头打开之后,403 响应里会带上完整的 openrouter_metadata 对象和一个描述 guardrail 各阶段的 pipeline 数组;而同一页更下面讲错误信息屏蔽的那一段,说的是「请求设置了 X-OpenRouter-Metadata 时」携带可选的路由上下文。两处的头名字不一致,官方文档原文里就是这么写的,我们没有依据判断哪一个是当前生效的写法,接入前请以官方文档最新内容为准。另外,带 Experimental 字样的这个头按其名称属于实验性能力,别把它当成稳定接口写进关键路径。

最后提一句排查工具:debug.echo_upstream_body 可以回显发给上游的请求体,但文档写明它只在流式模式下生效、非流式请求会忽略该参数,并明确警告不应在生产环境使用,因为可能返回请求里本不打算外泄的敏感信息。

以上代码片段与字段均照抄自官方文档;组合使用时,以上为按官方文档中的字段语义组合的示例,未经实测,以官方文档与 API 的实际响应为准。


本文依据 OpenRouter 官方文档(openrouter.ai/docs)于 2026-08-18 的公开内容整理。 该平台闭源,本文只复述官方文档写明的机制,不推断其内部实现我们没有对文中涉及的功能做过实测,因此不涉及界面外观与运行表现的任何描述。 该平台的供应商、模型与路由策略随时变动,文中不列具体供应商名单与模型清单; 价格、额度与限流的具体数值请以官方定价页与用量说明为准。

安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。

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