跳转到主要内容
你把一条 防火墙策略 绑定到一个密钥,模型流式回传 一个工具调用,而 response 执行面在你的智能体据此行动之前剥除或重写它。 执行决策在每个提供商上都相同——相同的规则、相同的判定、相同的事件。不同 的是一旦防火墙作用于一个流式工具调用后你的客户端看到的线上形态,因为 OpenAI chat、OpenAI Responses API 和原生 Claude /v1/messages 各自以不同 方式构造工具调用帧。 本页是关于那些客户可观察差异的聚焦说明。它不重新记录规则语言——参见 防火墙规则——也不重新记录执行面模型,那在 执行面 中讲解。关于三者共享的内部 挂起-重组机制,阅读 流式内部机制

1. 为什么防火墙提供商流式因线而异

在一个非流式响应上,防火墙一次性看到整个回复并决定。在一个上,模型的 工具调用作为一系列片段到达——一个帧里一个名称,参数 JSON 滴滴答答地跨更多 帧。一个判定需要完整的调用(名称完整参数),而一个工具调用片段 一旦被转发就无法收回。 所以在每个提供商上网关做同样的事:它让普通内容实时流式通过,并挂起 工具调用帧直到该调用被完整组装。在流结束时它评估每个组装好的调用,并只 发出幸存者——以那个提供商自己的事件形态。
你的文本绝不停顿。 只有工具调用帧被挂起。Assistant 内容、推理和角色帧 实时且原封不动地流式传输。挂起从第一个工具调用片段一直应用到那个回合的 结束——所以一个纯聊天响应的流式传输完全就像没有绑定防火墙一样。

2. OpenAI chat completions

/v1/chat/completions 上,工具调用作为按索引为键的 delta.tool_calls 片段流式传输。这道门挂起那些(以及遗留的 delta.function_call 形态),并在 收尾帧上发出从零重新索引的幸存调用,后跟一个 finish 帧:
结果你的客户端收到什么
allow原始挂起帧,逐字节——真正的 passthrough。
sanitize一个带重写参数的 tool_calls 增量,然后 finish_reason: "tool_calls"
deny(部分调用)只有幸存的调用,然后 finish_reason: "tool_calls"
deny(全部调用)没有工具调用,然后 finish_reason: "stop"——该回合看起来像模型选择用文本回答。
最后那一行是要据以测试的迹象:当一个防火墙从一个 OpenAI chat 回合中剥除 每一个工具调用时,你的智能体看到一个干净的 finish_reason: "stop",而非 一个错误帧。把你的循环构建为把”这个回合没有工具调用”当作一个有效结果。

3. OpenAI Responses API

原生 /v1/responses 流有它自己的事件模型——一个工具调用是一个 function_call 项,以 response.output_item.added 开启、流式传输 response.function_call_arguments.delta 片段、并在 response.output_item.done 完成。防火墙在 done 处评估,即该调用完整的第一个点:
一旦该调用通过,该项的 added / 参数增量 / done 事件被原封不动发出。
added 外壳流式传输,然后是一个其参数为脱敏版本的 done——原始的 参数增量片段被丢弃,所以未脱敏的值永不到达你。
缓冲的事件被丢弃,且被拒绝的项也被从你的客户端据以构建其最终状态的 终端 response.completed 对象中过滤掉——没有指向一个从未运行的调用的 悬空引用。
文本和推理增量自始至终实时流式传输,与在 chat completions 上完全一样。

4. 原生 Claude /v1/messages

一个原生 Anthropic 流是另一种东西:内容作为带索引的块到达—— content_block_startcontent_block_deltainput_json_delta 片段)→ content_block_stop——由一个携带 stop_reasonmessage_delta 收尾。 防火墙从第一个 tool_use 块开始挂起、评估每一个,并以连续索引重建幸存 的块,这样一个被剥除的块不留下索引缺口。 Claude 特有的迹象是 stop_reason。如果每个 tool_use 块都被拒绝,一个 tool_usestop_reason 会向你的客户端承诺一个永不到达的工具调用——所以 网关把它重写为 end_turn
upstream:  content_block_start (tool_use) … message_delta {stop_reason: "tool_use"}
            ↓ firewall denies the only tool_use
client:    (no tool_use block)            … message_delta {stop_reason: "end_turn"}
一次部分剥除保留幸存的 tool_use 块(连续重新索引),并让 stop_reason: "tool_use" 保持不变。
这适用于原生 Claude 流。一个通过 OpenAI 格式端点调用的 Claude 模型则 在 OpenAI chat 线上执行(§2),所以它显示 finish_reason: "stop",而非 stop_reason: "end_turn"。把你的回合结束处理匹配到你调用的线格式,而非 底层模型。

5. 一个具体示例

同一条规则在每个提供商上产生同样的决策——只有你的客户端读取的线上形态 不同。在 response 执行面上编写它一次:
{
  "stage": "response",
  "tool_name_glob": "shell.exec",
  "verdict": "deny",
  "args_match_json": "{\"clauses\":[{\"path\":\"$.command\",\"op\":\"regex\",\"value\":\"rm -rf|mkfs\"}]}"
}
以三种方式流式传输同一个提示词,防火墙每次都拒绝那个 rm -rf 调用。你的 客户端观察到的:
线全部剥除后的终端信号
OpenAI chatfinish_reason: "stop"
OpenAI Responses项从 response.completed 中缺席
原生 Claudestop_reason: "end_turn"
匹配并被拒绝的调用在 防火墙事件 中无论 哪条线都相同地显示,所以即便流不是提供商无关的,你的可观测性是。

6. 跨提供商保持不变的东西

线不同;约定不变:
  • 判定和规则是线无关的。 allow / audit / deny / sanitize 在每个 提供商上意思相同。参见 判定
  • Sanitize 只触碰工具调用参数,绝不触碰工具返回的内容——在每条线上。 参见 脱敏响应
  • Allow 是真正的 passthrough。 当防火墙不采取行动时,挂起的帧作为确切 的上游字节被重放——无重新批处理,无丢失提供商特定字段。
  • 影子模式在各处都适用。 打开它,挂起的工具调用总会幸存(降级为 audit),这样你能在一条策略改变流量之前跨提供商衡量它的影响。参见 影子模式

7. 这部分的位置

流式内部机制

每个提供商共享的挂起-组装-重组机制。

执行面

为什么流式工具调用执行存在于 response 执行面上。

判定

一个流式调用解析到的提供商无关决策。

Response 过滤

对一个模型发出的工具调用设门,无论是否流式。
关于这些流式检查应对的威胁,参见 危险工具调用数据外泄;关于流执行在请求路径中 的位置,参见 执行路径延迟