跳转到主要内容
当某个 防护栏防火墙 拦下一个请求时,OrcaRouter 返回一个你的代码可以分支处理的类型化错误—— 而不是一个含糊的 400。三个安全错误码覆盖了你会遇到的情形:一个被筛查的 提示词或响应、一次被拒绝的工具调用,以及一次被挂起等待人工审批的工具调用。 本页是这些错误码的参考——每一个的使用场景、确切的 HTTP 状态、它给你带来 的代价,以及那条最重要的规则:重试逻辑必须对它们做特殊处理。 三者都被 标记为 skip-retry;盲目地重新运行同一个调用只会再次触发同一个控制。
这些是*执行(enforcement)*错误码——网关决定转发你的调用。它们有别于 上游提供商错误(一个模型 429、一次上下文溢出),也有别于认证失败。要了解 某个具体请求为何被拦下,参见 为什么这次被拦下了?

1. llm 安全错误码一览

每一次安全拦截都返回 HTTP 400,带有一个 OpenAI 形态的错误体 (error.code 是下面那个类型化字符串)。在原生 Claude(/v1/messages) 路由上,同一个错误码以 Claude 错误形态传递,因此 SDK 路由在各协议间 是确定的。
错误码拦下什么配额代价
guardrail_blocked一个命中 block 规则的提示词或响应
firewall_blocked一次被拒绝的工具调用 / 声明不消耗模型 token
firewall_approval_pending一次被挂起等待人工审查者的工具调用不消耗模型 token
error.code 做分支,而非消息字符串。消息会点名具体的防护栏、规则或 工具,并且会变化;错误码则是一份稳定的契约。

2. guardrail_blocked —— 一个被筛查的提示词或响应

当一条带 block 动作的 防护栏 规则触发时返回—— 一个被拒绝列表收录的关键词、一次正则命中、一个你选择拦截而非脱敏的 PII 或 密钥实体、一个 llm_judge 判定,或一次失败的 grounding 检查。 HTTP 400。 消息点名触发的防护栏和规则。
一个被拦截的请求不消耗配额。输入阶段拦截在计量之前触发,因此 永远不会有计费。输出阶段拦截在模型响应之后运行,因此网关会在返回 错误之前退回预先扣除的配额。无论哪种方式,你都不为一次被拦截的 调用付费。
判定是内容的属性,而非通道的属性。重新运行同一个提示词——即便对照 一个不同的模型——也会产生同一个拦截。修复输入(或策略),而不是重试。
mask 规则返回这个错误码。一个被脱敏的匹配(例如 jane@acme.com[EMAIL])会就地脱敏,调用照常进行——你得到一个 200,只是去掉了那段敏感内容。只有 block 动作会浮现 guardrail_blocked。 (flag 完全不改变流量。)
{
  "error": {
    "type": "openai_error",
    "code": "guardrail_blocked",
    "message": "request blocked by guardrail \"pii-shield\": rule ssn (block)"
  }
}
关于这个错误码背后的规则类型、阶段和动作,参见 防护栏。关于逐字段的错误信封,参见 Webhook 与错误负载

3. firewall_blocked —— 一次被拒绝的工具调用

防火墙 为一次工具调用解析出一个 deny 判定时 返回——一条破坏性的 shell 命令、一次 SSRF 形态的 fetch、一个在拒绝列表上的 egress 目的地,或一个处于 block 模式的 技能 这次拒绝如何浮现,取决于 执行面

inbound / response / egress

HTTP 400error.code = firewall_blocked。错误体携带结构化的 error.metadatareason_code、风险 factorsrisk_score),因此 你能解释这次拦截,而不只是看到它。

mcp 执行面

作为一个工具错误firewall deny: <reason>)返回,而非一次传输 失败——这样模型能看到这次拒绝并换一个工具、询问用户,或停止,而不是 让整个运行崩溃。
sanitize 不是一次拦截。一个 sanitize 判定会从工具调用参数中 脱敏匹配的子串并转发清理后的调用——它永远不会返回 firewall_blocked。 (唯一的例外:在 inbound 执行面上,此时还没有调用时参数,sanitize 会 升级为拒绝。)
{
  "error": {
    "type": "openai_error",
    "code": "firewall_blocked",
    "message": "tool \"shell.exec\" blocked by firewall: denied tool",
    "metadata": {
      "reason_code": "FW-TOOL-001",
      "risk_score": 92,
      "factors": ["denied_tool"]
    }
  }
}
就配额而言,一次 inbound 拦截在上游模型调用之前触发,因此不消耗模型 token。关于每一个判定,参见 判定词汇表;关于这个错误码所 防御的威胁,参见 危险的工具调用

4. firewall_approval_pending —— 被挂起等待人工

在一次工具调用命中 pending_approval 判定的那一刻返回。一个人工介入 (human-in-the-loop)门控不能是一次阻塞式的内联等待,因此网关会立即返回 一个**已挂起(held)**响应,而不是长轮询。 HTTP 400。 该错误携带审批 id,让你的智能体知道要解决哪一次挂起。 这是唯一一个你应当通过解决并重新提交来应对的错误码——而不是把它当作 一个终结性失败:
1

从已挂起的错误中读出审批 id

该 id 可从错误体中恢复。先别重试这次调用——一次幼稚的重试只会再次 挂起。
2

等待一个决策

一名审查者从控制台(Developer+)解决它,或者你的审批系统收到一个 HMAC 签名的 webhook 回调。你的智能体轮询 GET /api/v1/firewall/approvals/:id 获取状态。
3

携带审批令牌重新提交

一旦获批,携带一次性的 X-OrcaRouter-Firewall-Approval 头重新发出 原始调用。网关认出这个 id 并放行那一次调用。
审批路由(/api/v1/firewall/approvals/*)运行在一个 firewall-gateway-scoped 密钥上,而非你的控制台会话。完整的环路参见 人工审批(HITL),回调签名参见 Webhook 负载

5. 为什么三者都跳过重试

标准 SDK 重试逻辑假设一个 400 在第二次尝试时可能成功。这些错误码打破了 这一假设——拦截是确定性的,因此一次盲目重试会浪费一次往返,并(对挂起的 调用而言)静默地把一次审批重新入队。
OrcaRouter 自己的内部重试/回退机制绝不会针对另一个通道重新尝试一个 返回了这些错误码之一的调用。在你的客户端里照做:遇到一个安全错误码时, 停下并按判定行事,不要循环。
  • guardrail_blocked → 修复输入或放宽策略;把拒绝浮现给用户。不要重试。
  • firewall_blocked → 该动作不被允许;让智能体选择一个不同的工具或 请求帮助。不要重试。
  • firewall_approval_pending → 解决这次挂起,然后携带审批头重新提交 一次(§4)。一次不带审批头的重试会再次挂起。

6. 配额与计费小结

一次安全拦截绝不会为被拦截的那一份工作向你计费。
错误码何时触发计费结果
guardrail_blocked(输入)在模型调用之前从不计量
guardrail_blocked(输出)在模型响应之后退回预先扣除的配额
firewall_blocked(inbound)在模型调用之前不消耗模型 token
firewall_approval_pending在派发之前不消耗模型 token
一条防护栏的 llm_judgegrounding 规则确实会调用一个模型来得出 它的判定,而那些 judge token 会作为一个单独的 judge 子项计费——即便判定 是一次拦截。那是检查本身的成本,而不是被拦截请求本身的成本。

7. 相关参考

为什么这次被拦下了?

把一次拦截追踪到产生它的确切规则、执行面和原因。

判定词汇表

每一个防火墙判定——allow、audit、deny、sanitize、pending_approval、 cap_cost——以及各自浮现什么。

Webhook 与错误负载

完整的错误信封、error.metadata 字段,以及审批回调签名。

执行模式

Shadow、observe 与 enforce——一个判定何时真正改变流量。
关于产生这些错误码的控制,参见 防护栏防火墙;关于词汇,参见 概念词汇表