OpenRouter 报错 403 怎么解决:权限不足与 guardrail 拦截的区分

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

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

先把结论说完:OpenRouter 的 403 只表达一个意思——这次请求”不让做”,而不是”不认识你”(那是 401)或者”没额度了”(那是 402)。 官方错误码清单里,Forbidden 这一档的括号里写着三种成因:权限不足、guardrail 拦截、内容审核标记;在更精确的 error_type 词表里它对应 permission_denied,官方描述是 key 是有效的,但缺少所需权限,或者请求被 guardrail 拦截。所以排 403 的第一刀不是去翻 key、也不是去查地区,而是先分清这次拦截发生在 guardrail 的运行时内容检查上,还是发生在权限与作用域配置上——官方给了一个可以直接看出来的信号,拿到它之后基本不用猜。

一、403 和 401、402 的分界线在哪

OpenRouter 的错误响应结构是固定的:error 对象里有 codemessage,还有一个可选的 metadata。请求在到达模型之前就被拒时(比如请求本身非法、或账户额度不足),HTTP 状态码与 error.code 一致;如果模型已经开始产出,之后才出的错另有一套返回方式,走响应体或 SSE 事件。403 属于前一类。真正有诊断价值的是 error_type 这个字段,官方把它称作跨所有协议格式都可依赖的那个字段——因为原生协议码是有损的,同一类问题在不同接口皮肤下可能被折叠成不同的名字。

鉴权与授权那一组一共三个取值(以官方文档为准):

error_type官方含义该往哪查
authenticationkey 缺失、无效或已吊销查 key 字符串与请求头
permission_deniedkey 有效,但缺少所需权限,或被 guardrail 拦截查权限与作用域,查 guardrail
payment_required账户或该 key 额度不足查余额与 key 上的消费上限

这三者的处理方向完全不同,认错一个就要多花一轮时间。401 的细分排查可以看站内的OpenRouter 报错 401 排查顺序那篇,这里不重复。

还有一个跨接口的细节值得记住:如果你走的是 Anthropic Messages 那套接口皮肤,permission_denied 会被映射成 Anthropic 原生的 error.typepermission_error。官方明确说明,因为原生类型有损,规范化后的 error_type 会被额外放进 error 对象里一并返回。也就是说,即便你用的不是 Chat Completions,也仍然能拿到那个更精确的字段。

二、判断是不是 guardrail 拦的:看 pipeline

这是本文最有用的一节。官方在 guardrails 文档里写了一句话,直接把 403 分成了两半:

当 guardrail 的运行时检查拦下一个请求(比如内容过滤器或提示注入检测器),OpenRouter 返回 HTTP 403 Forbidden;预算上限和允许列表限制同样会产生 403,但只有运行时内容检查会带上 openrouter_metadata 的阶段明细

这句话给了一个几乎零成本的判据:响应里有没有 guardrail 阶段明细,就能把”内容被拦”和”配置不让你用”分开

拿到明细需要主动开启。官方的做法是发请求时带上 X-OpenRouter-Metadata 头,值为 enabled;不带就不返回。文档同时说明旧的头名 X-OpenRouter-Experimental-Metadata 仍然被接受,只是建议迁移到新名字(以官方文档当前版本为准)。开启之后,错误响应里的 openrouter_metadata 会挂在顶层,是 error 的兄弟节点而不是嵌在里面——这一点和成功响应的位置保持一致,写解析代码时别找错层级。官方说明这条规则适用于 Chat Completions、Messages、Responses 和旧版 Completions 四条路由,流式与非流式都算。

openrouter_metadata 里最关键的是 pipeline 数组。官方对它的定义是:记录每一个实质性影响了请求的插件;没有真正生效的插件不会留下阶段记录。guardrail 类的阶段 typeguardrail,官方列出的 name 取值包括 content-filtermoderation。阶段里带 guardrail_idguardrail_scope(比如 api-key)、一段 summary,以及一个自由结构的 data,里面会说明 action、是否 detected、用了哪些 engines、命中了哪些 patterns

官方还给了两条写解析逻辑时的忠告,值得照做:

  • 多个插件可能共用同一个 type。要找某个具体的 guardrail,就同时匹配 typename;想一次性抓全部 guardrail 类插件,按 type === 'guardrail' 过滤即可,不必逐个枚举名字
  • 阶段类型的列表会随时间增长,未知类型当作不透明对象处理,不要写死枚举

一个容易踩的坑:官方说明缓存命中永远不会带 openrouter_metadata,流式和非流式的缓存重放都会把这个字段剥掉,理由是缓存里的路由信息可能已经过时。所以”这次没有 pipeline”不等于”这次没走 guardrail”,得先确认这不是一次缓存命中。

三、guardrail 侧的几条具体拦截路径

如果 pipeline 确认是 guardrail 拦的,接下来看是哪一类。官方文档里能拦出 403 的运行时检查有这么几条:

自定义内容过滤规则。 每个 guardrail 可以挂一组正则模式,每条模式绑一个动作:Redact 是把命中片段替换成占位符后照常转发;Block 是在请求到达模型之前直接拒绝,返回 403。官方还明确列了哪些正则特性不被允许:前瞻、后顾、反向引用(数字与命名两种),以及像嵌套量词那样容易引发过度回溯的写法。这些模式在创建或更新时就会被 API 以 invalid_regex_pattern 错误挡下来——所以如果你是规则的配置方,报错会出现在配置阶段而不是请求阶段。

提示注入检测。 官方的正则检测有三档动作:Flag 只记录不拦截,请求原样转发;Redact 把命中片段替换成 [PROMPT_INJECTION] 后转发;Block 直接以 403 拒绝。这里有一条很实用的合并规则:当多个 guardrail 同时作用于一个请求时,最严的动作胜出,优先级是 block 大于 redact 大于 flag。检测本身除了正则,还包含针对规避手法的处理——把中间字母打乱的变形写法、Base64 与十六进制编码内容(解码后再查关键词)、以及字符间插空格的写法(折叠空格后重新扫描)。

敏感信息拦截。 命中且动作为 Block 时同样返回 403,错误消息的形态是 Request blocked by content filter: [LABEL]。这个 LABEL 从哪来,官方列了四种情况:内置预设用预设名;自定义模式若配了 label 用你写的标签;自定义模式没配标签就是 [BLOCKED];NLP 识别出的实体则用实体类型。这意味着错误消息本身就是最直接的线索——它告诉你是哪一条规则拦的。

预算上限。 工作区预算按周期配置,官方给的周期取值是 daily、weekly、monthly、lifetime 四种。请求进来时会拿当前花费和每一条已配置的预算比对,任意一条达到或超过就返回 403 Forbidden,错误消息会点名是哪个周期的预算被突破了。注意这条同样是 403,但按上一节的规则,它不会带 guardrail 阶段明细。

四、“我明明放开了”——层级合并把你坑了

这是 guardrail 类 403 里最反直觉的一类。官方的层级规则是这样的:账户级的隐私与供应商设置永远作为一条默认 guardrail 生效;在此之上还可以按组织成员、按 API key 两级分配。合并方式官方写得很明确——供应商允许列表取交集,模型允许列表取交集,敏感信息过滤器取并集(同一实体类型出现不同动作时,block 优先于 redact)。一句话:更严的规则永远赢。

所以当你在某一层”放开”了某个模型或供应商,却仍然 403,正确的排查方向是去找还有哪一层没放开,而不是反复检查你刚改的那一层。官方专门举了这个例子:成员级 guardrail 允许 A、B、C 三家供应商,而 key 级只允许 A、B,那么这个 key 实际可用的就只有 A 和 B。

还有两条边界条件容易忽略:

  • 一个用户或一个 key 只能被直接分配一条 guardrail。组织成员创建的所有 API key 都会隐式跟随该成员的 guardrail 分配,即便这些 key 自己又被更严地限制了一遍
  • guardrail 创建出来不等于生效。官方用警告框强调:guardrail 在被分配之前什么也不强制;传 workspace_id 只是把它归入某个工作区做组织管理,并不会应用到该工作区的流量上。想让整个工作区的流量都受限,要去配工作区的默认 guardrail

官方还提供了一个”资格预览”(Eligibility Preview):查看某条 guardrail 时能看到它与你的账户设置组合之后,实际还剩哪些供应商和模型可用。在分配之前先看这个预览,比事后对着 403 反推要快得多。

五、不是 guardrail 的那一半:权限与作用域

如果 pipeline 里没有 guardrail 阶段,也排除了预算,那就该往”这个 key 本来就不该做这件事”的方向查了。官方文档里散落着几条明确会返回 403 的场景:

用错了 key 的类型。 OpenRouter 的管理密钥(Management API Key)和推理密钥是两套东西,且互不通用:官方说明管理密钥不能用于调用补全类端点;反过来,分析类 API 需要管理密钥,拿普通推理 key 去调会得到 403。这条在排查里很有用——如果你的 403 出现在某个管理类或统计类接口上,先确认你带的是不是管理密钥。

参数与鉴权方式不匹配。 创建 API key 的接口里,external_userexternal_api_key 这两个字段官方规定只在使用 Connect client secret 鉴权时才被接受(其中 external_user 在那种场景下是必填的);用管理密钥却带上这两个字段中的任意一个,会被以 403 拒绝。这类 403 的特征是:换 key 没用、加权限也没用,因为问题出在请求体上。

跨工作区的作用域。 文件类接口的归属规则是每个文件属于一个工作区,默认走 key 所属的工作区,也可以用 workspace_id 查询参数指定。官方写明:作用域在另一个工作区的 key 会得到 403,而来自另一个工作区的文件 id 得到的是 404。两个码分别对应两件事,别混。另外,当工作区的存储配额被占满时,上传同样返回 403(具体容量以官方文档为准)。

端点选错了。 文件类接口官方说明只在全局端点上工作,走企业版的区域内路由端点会返回 403。这是本文里唯一一条和”域名”有关的 403,但它说的是你调的是哪个端点域名,不是别的。

BYOK 场景下的 403 来自上游。 如果你用的是自带供应商密钥的模式,403 的含义变成了”你在那家云厂商那里权限不够”。官方给的排查路径是:去 Activity 页找到那次生成,点进详情看原始元数据,找 provider_responses 字段——它是一个数组,逐条记录了路由过程中每次供应商尝试的名称与 HTTP 状态码。官方对这类 403 的具体建议是检查上游的授权策略是否包含调用模型所需的权限项,并且到该云厂商自己的控制台里先试着直接调一次模型。

OAuth 授权码流程。 官方在这条链路上列了两个 403 文案:一个是授权码或 code_verifier 无效,要确认用户已登录 OpenRouter 且 code_verifiercode_challenge_method 正确;另一个是授权码过期——官方明确说授权码在签发后 10 分钟失效,要重新走一遍流程并尽快兑换。

六、为什么不该按”地区限制”去排 403

这是中文语境里对 403 最常见的误判。官方错误码清单给 Forbidden 列出的成因就是那三项:权限不足、guardrail 拦截、内容审核标记,没有”按访问者所在地区拒绝”这一条。按地区去查,方向从一开始就错了,而且这条弯路特别费时间,因为它会把你引向一堆和账号本身无关的猜测。

文档里唯一把 403 和”地理”放在一起的地方,是写给供应商接入方看的可用率统计口径:那一节在定义某个供应商端点的可用率怎么算,说明限流(429)和地理限制(403)都单独统计、不计入该端点的可用率。那是在描述上游端点的健康度指标,和你账号侧收到的 403 是两件事。

顺带说一句内容审核。官方在 Forbidden 档里列了”内容审核标记”这一项,而审核类错误的响应有自己的元数据结构:会带上被标记的原因列表、被标记的那段文本(超长时从中间截断并以省略号替代)、发起审核的供应商名,以及模型标识。看到这几个字段,就说明拦截来自内容审核而不是你的权限配置。

七、误报了怎么办,以及一条可照着走的顺序

先说误报。官方在 Logs 页提供了反馈入口:带 guardrail 事件的生成记录那一行会显示一个盾牌图标,悬停打开弹层;单条模式命中时可以直接在弹层里标记为误报,多条命中时弹层会跳到生成详情页,在那里勾选具体命中项再提交。但有一句提醒必须记住:标记误报不会追溯性地解封那次请求——如果动作是 block,原请求早已被拒。

另一个补救入口是提示注入检测的白名单,可以把已知安全的短语排除在检测之外。它的边界官方也写清楚了:白名单作用于正则检测的那些模式,对规避检测器(乱序字母变形、Base64 与十六进制解码)不生效,因为那些检测器工作在解码或归一化之后的文本上,按短语豁免没有意义。

官方给配置方的建议也很务实:先用 Flag 模式在真实流量上跑,测出误报率,确认可以接受之后再切到 Redact 或 Block。敏感信息这一侧同理,官方建议起步就用 Redact 而不是 Block,让请求先能跑通。

最后把排查压成一条顺序:

  1. 确认状态码与 error_type,是 permission_denied 才继续往下走;是 authenticationpayment_required 就走别的路
  2. 带上元数据头重发一次,看顶层的 openrouter_metadata.pipeline 里有没有 typeguardrail 的阶段。有 → 内容被拦;没有(且不是缓存命中)→ 权限、作用域或预算
  3. 读错误消息本身:内容过滤的消息会点名 LABEL,预算的消息会点名周期,这两类不需要额外工具就能定位
  4. 是内容被拦:按 block / redact / flag 三档确认动作,检查多条 guardrail 的合并优先级,必要时用白名单或误报反馈
  5. 是配置问题:按”更严的赢”逐层往上找没放开的那一层,用资格预览确认实际可用范围
  6. 都不是:检查 key 类型(管理密钥与推理密钥)、请求体里有没有和鉴权方式不匹配的字段、key 的工作区作用域、以及是不是调错了端点
  7. BYOK 场景:去 Activity 详情里读 provider_responses,把问题定位到具体那一次上游尝试

最容易栽的坑还是第一条:很多人看到 403 的第一反应是换个 key 再试一次。403 恰恰意味着 key 是被认可的,换 key 大概率什么也解决不了,只会掩盖真正的原因。想把整套错误码的分工看得更全,可以再看站内的通用 API 401/403 排查思路OpenRouter 429 速率限制处理

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

留言讨论

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

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

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

    OpenRouter 充值不方便?

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

    看替代方案

    这个页面有问题?

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