response убирает или переписывает
его до того, как ваш агент на него отреагирует. Решение применения идентично
у каждого провайдера — те же правила, те же вердикты, те же события. Что
отличается — это форма провода, которую видит ваш клиент, когда firewall
подействовал на стримированный вызов инструмента, потому что OpenAI chat,
OpenAI Responses API и native Claude /v1/messages каждый кадрирует вызовы
инструментов по-разному.
Эта страница — сфокусированная заметка об этих наблюдаемых клиентом различиях.
Она не передокументирует язык правил — см.
Правила Firewall — или модель стадий, покрытую в
Стадиях. О внутреннем механизме удержания и
пересборки, общем для всех трёх, читайте
Внутренности стриминга.
1. Почему firewall provider streaming отличается по проводу
В нестримированном ответе firewall видит весь ответ сразу и решает. На стриме вызов инструмента модели приходит как последовательность фрагментов — имя в одном фрейме, JSON аргументов, размазанный по многим другим. Вердикту нужен полный вызов (имя и полные аргументы), а фрагмент вызова инструмента, будучи переслан, нельзя отозвать. Так что у каждого провайдера шлюз делает одно и то же: он пропускает обычный контент стримиться вживую и удерживает фреймы вызовов инструментов, пока вызов полностью не собран. В конце стрима он вычисляет каждый собранный вызов и выдаёт только выживших — в собственной форме событий этого провайдера.Ваш текст никогда не застревает. Удерживаются только фреймы вызовов
инструментов. Контент ассистента, рассуждение и фреймы role стримятся вживую и
без изменений. Удержание применяется с первого фрагмента вызова инструмента до
конца этого хода — так что только-чат ответ стримится ровно так, будто никакой
firewall не привязан.
2. OpenAI chat completions
На/v1/chat/completions вызовы инструментов стримятся как фрагменты
delta.tool_calls, привязанные к индексу. Шлюз удерживает их (и legacy форму
delta.function_call) и, на закрывающем фрейме, выдаёт выжившие вызовы,
переиндексированные с нуля, за которыми следует фрейм завершения:
| Исход | Что получает ваш клиент |
|---|---|
| allow | Оригинальные удержанные фреймы, побайтно — настоящий passthrough. |
| sanitize | Одна дельта tool_calls с переписанными аргументами, затем finish_reason: "tool_calls". |
| deny (некоторые вызовы) | Только выжившие вызовы, затем finish_reason: "tool_calls". |
| deny (все вызовы) | Нет вызова инструмента, затем finish_reason: "stop" — ход выглядит так, будто модель решила ответить текстом. |
3. OpenAI Responses API
native-стрим/v1/responses имеет собственную модель событий — вызов
инструмента — это элемент function_call, который открывается с
response.output_item.added, стримит фрагменты
response.function_call_arguments.delta и завершается на
response.output_item.done. Firewall вычисляет на done, первой точке, где
вызов целен:
allow → буферизованные события выгружаются дословно
allow → буферизованные события выгружаются дословно
События
added / дельта-аргумента / done элемента выдаются без
изменений, как только вызов проходит.sanitize → оболочка элемента + переписанный done
sanitize → оболочка элемента + переписанный done
Оболочка
added стримится, затем done, чьи аргументы — отредактированная
версия — оригинальные фрагменты дельта-аргумента отбрасываются, так что
неотредактированное значение никогда вас не достигает.deny → элемент убирается везде
deny → элемент убирается везде
Буферизованные события отбрасываются, и отклонённый элемент также
отфильтровывается из терминального объекта
response.completed, из
которого ваш клиент строит своё финальное состояние — нет висячей ссылки на
вызов, который никогда не выполнялся.4. Native Claude /v1/messages
native-стрим Anthropic — другой зверь: контент приходит как индексированные
блоки — content_block_start → content_block_delta (фрагменты
input_json_delta) → content_block_stop — закрытые message_delta, несущим
stop_reason. Firewall удерживает с первого блока tool_use, вычисляет
каждый и реконструирует выжившие блоки с непрерывными индексами, так что
убранный блок не оставляет пробела в индексах.
Специфичный для Claude признак — stop_reason. Если каждый блок tool_use
отклонён, stop_reason равный tool_use обещал бы вашему клиенту вызов
инструмента, который никогда не приходит — так что шлюз переписывает его на
end_turn:
tool_use, переиндексированные
непрерывно, и оставляет stop_reason: "tool_use" нетронутым.
5. Один конкретный пример
То же правило производит то же решение у каждого провайдера — отличается только форма провода, которую читает ваш клиент. Создайте его один раз, на стадииresponse:
rm -rf
каждый раз. Что наблюдает ваш клиент:
| Провод | Терминальный сигнал после полного убирания |
|---|---|
| OpenAI chat | finish_reason: "stop" |
| OpenAI Responses | элемент отсутствует в response.completed |
| Native Claude | stop_reason: "end_turn" |
6. Что остаётся постоянным у разных провайдеров
Провод отличается; контракт — нет:- Вердикты и правила независимы от провода.
allow/audit/deny/sanitizeозначают одно и то же у каждого провайдера. См. Вердикты. - Sanitize трогает только аргументы вызова инструмента, никогда содержимое, которое возвращает инструмент — на каждом проводе. См. Очистку ответов.
- Allow — это настоящий passthrough. Когда firewall не предпринимает действий, удержанные фреймы воспроизводятся как точные байты upstream — нет пере-батчинга, нет потерянных специфичных для провайдера полей.
- Shadow-режим применяется везде. Включите его, и удержанные вызовы
инструментов всегда выживают (понижены до
audit), так что вы можете измерить влияние политики у разных провайдеров до того, как она изменит трафик. См. Shadow-режим.
7. Куда это вписывается
Внутренности стриминга
Механизм удержания-сборки-пересборки, общий для каждого провайдера.
Стадии
Почему применение к стримированным вызовам инструментов живёт на поверхности
response.Вердикты
Независимые от провайдера решения, в которые разрешается стримированный вызов.
Фильтрация response
Ограничение вызовов инструментов, которые выдаёт модель, на стриме или нет.
