OpenRouter 报错 401 是什么原因?鉴权失败的排查顺序
数据截至 2026-08,价格与限额以各官网为准。本文只讲计费与接入机制,不列具体价格数字。
先把结论说完:OpenRouter 的 401 只有一个含义,就是鉴权没通过,官方的原话是 API key 无效或缺失,让你确认 key 正确并且确实放进了 Authorization 头。 它不代表你没钱、不代表你没权限、也不代表你被限流了——这三种情况在 OpenRouter 那里各有各的状态码和各自的 error_type,混淆它们是排查时最常见的时间浪费。本文做两件事:第一,把 401 和它最容易被认错的三个邻居彻底分开;第二,给出一个从”错误是不是真的 401”开始、逐层往下的排查顺序。
第一步:先确认它真的是 401
这一步看起来多余,但它能省掉后面所有的无用功。OpenRouter 的错误响应里除了 HTTP 状态码,还有一个更精确的 error_type 字段,官方按类别把它们分成了几组。和鉴权相关的那一组是这样的:
authentication—— API key 缺失、无效,或者已被吊销permission_denied—— key 是有效的,但缺少所需权限,或者请求被 guardrail 拦截payment_required—— 账户或该 key 的额度不足
三者的区别值得单独强调一遍,因为它们的处理方向完全相反:
| 你看到的 | error_type | 真实含义 | 该往哪查 |
|---|---|---|---|
| 401 | authentication | key 本身不被认 | 查 key 字符串、查请求头 |
| 403 | permission_denied | key 认了,但这件事不让你做 | 查权限配置、查 guardrail |
| 402 | payment_required | key 认了,权限也有,就是没额度 | 查账户余额、查 key 上的消费上限 |
特别提醒 403:它的官方定义是权限不足或被 guardrail 拦截,不是很多人想当然以为的”地区限制”。按地区去排查 403,方向从一开始就是错的。
如果你的客户端只把状态码打出来、没打 error_type,第一件该做的事是把响应体完整打印出来。官方的错误响应结构里带着 metadata.error_type,有了它,后面的排查能少走一大半弯路。
第二步:区分 key 缺失、无效、已吊销
官方把这三种情形都归在 authentication 下面,但它们的成因和解法并不一样。
缺失是最容易发生也最容易忽略的一种。它未必是你没写鉴权头,更常见的形态是环境变量在当前进程里是空的——比如你在一个 shell 里设置了变量,却在另一个 shell 或另一个服务进程里发的请求;或者变量名拼错了,读出来是空字符串,最后拼成了一个只有 Bearer 前缀、后面什么都没有的请求头。判断方法很直接:在发请求之前先把你要用的那个变量本身打印出来看长度,而不是相信它应该有值。
无效指 key 字符串本身不被接受。除了复制粘贴时漏字符、带上了首尾空格或换行这类问题,还有一种情况是你用错了账户——同一个人在个人账户和组织账户下各建过 key,拿了 A 账户的 key 去调 B 账户配额下的资源。
已吊销是官方在定义里专门列出来的第三种,也是最容易被漏掉的一种。key 被删除或轮换之后,旧 key 就不再有效,但你的某个服务、某台机器、某个定时任务可能还在用旧的那份。这种故障有个典型特征:大部分调用是好的,只有某一个入口在报 401。如果你遇到的是这种”部分失败”,几乎可以直接跳到下一节。
第三步:核对 Authorization 头的形式
官方示例里给出的鉴权头形式是 Authorization: Bearer <OPENROUTER_API_KEY>,也就是”Bearer + 一个空格 + key”。对着这个标准逐项核对,比凭印象改要快:
- 头的名字是
Authorization,不是Auth,也不是X-Api-Key Bearer和 key 之间是一个空格,前后没有引号- key 本身没有被换行截断——从网页复制长字符串时,这件事发生的概率比你以为的高
另外有一个值得澄清的点:官方示例里还出现了 HTTP-Referer 和 X-OpenRouter-Title 两个头,文档明确说明它们是可选的,作用是让你的应用出现在 OpenRouter 的榜单上。它们和鉴权没有关系,缺了不会导致 401,加了也不能解决 401。排查时不必在它们身上花时间。
如果你用的是 OpenAI 官方 SDK 指向 OpenRouter 的方式(官方支持这种 drop-in 用法,把 baseURL 设为 OpenRouter 的 v1 地址即可),那么 key 是通过 SDK 的 apiKey 参数传的,鉴权头由 SDK 自己拼装。这种情况下要检查的不是头本身,而是你传给 SDK 的那个值——以及一个很隐蔽的点:SDK 通常会在你没显式传参时去读它自己约定的环境变量,如果那个变量里还留着别家平台的 key,SDK 会把它拿去用,结果就是一个让人摸不着头脑的 401。显式传参可以彻底避免这类问题。
第四步:如果是轮换 key 之后开始报的
OpenRouter 官方文档有专门讲 key 轮换的一节,给出的顺序是三步:先创建新 key,再更新所有应用,最后才删除旧 key。
这个顺序不能颠倒,原因就是上面说的”部分失败”。如果你先删旧 key 再去改配置,那么在两个动作之间的所有请求都会 401;而如果你有多个服务共用一份 key,很可能改完了主服务、忘了某个后台任务,等到几小时后那个任务被触发才暴露出来。
所以当你面对的是”轮换之后开始报 401”,排查方向不是去研究 key 对不对,而是去清点还有谁在用旧的那份。按部署单元逐个过:主服务、后台任务、定时脚本、本地开发环境、CI 配置、以及某个同事机器上的 .env。
顺带一提,OpenRouter 支持给单个 key 配置消费上限,查询 key 信息的接口会返回上限、重置周期和剩余量这几个字段。这个接口在排查时有个额外用途:它能同时验证 key 的有效性和它的额度状态——如果调它就返回鉴权错误,说明问题确实在 key 本身;如果它正常返回、只是剩余量见底,那你面对的其实是 402 不是 401。关于 key 的日常管理,站内还有一篇API Key 安全管理可以参考。
第五步:第三方客户端与网关场景
如果你不是直接发 HTTP 请求,而是通过某个桌面客户端、编辑器插件或者自建网关在用 OpenRouter,401 的产生位置就多了一层。
关键在于先定位是哪一段在报错。最简单的办法是绕开中间层:用一个最小的直连请求(把 key 放进 Authorization 头,调一个最简单的端点)验证 key 本身。如果直连是通的,那问题就在中间层的配置里,而不在 key 上;如果直连也 401,那就回到前面几步。
自建网关的场景还要多留意一层:网关上通常有它自己的一套令牌体系,你的客户端拿的是网关发的令牌,网关再拿真实的 OpenRouter key 去请求上游。这时候”401”可能来自两个完全不同的地方,看错了就会一直在错误的一侧打转。判断依据是错误响应的形态——上游返回的错误会带着 OpenRouter 的响应结构和 error_type 字段,网关自己拒绝的通常不会。想了解这类架构的取舍,可以看站内的API 中转的安全考量。
排查顺序小结
把上面的内容压成一条可以照着走的顺序:
- 打印完整响应体,确认
error_type是不是authentication。是payment_required或permission_denied就走别的路 - 打印你实际用的 key 变量,确认它非空、长度正常、首尾无空白
- 对照官方形式核对请求头:
Authorization: Bearer <key>,一个空格,无引号 - 如果是部分入口失败,按部署单元清点谁还在用旧 key
- 用 key 查询接口验证:能正常返回说明 key 有效,问题在别处;返回鉴权错误说明确实是 key 的问题
- 如果经过中间层,先用最小直连请求把两侧分开
最后提醒一个反直觉的现象,它经常被误判成鉴权问题:账户余额为负时,请求有可能失败,而且这种失败会波及免费模型。官方限额说明的措辞是「可能会看到错误,包括免费模型」,解法是把余额补到零以上。如果你某天发现所有模型突然全都调不通、包括原本免费的那些,先去看余额,别一头扎进 key 里。这属于 402 的范畴,具体可以对照OpenRouter 额度管理那篇。
留言讨论
评论发布后会被人工复核,违规内容将被删除。
如果发表没有反应,可以前往联系我们告诉我们。