Перейти к основному содержанию
Вы привязываете политику firewall к ключу, модель стримит вызов инструмента обратно, и стадия 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" — ход выглядит так, будто модель решила ответить текстом.
Последняя строка — это признак для тестирования: когда firewall убирает каждый вызов инструмента из хода OpenAI chat, ваш агент видит чистый 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, первой точке, где вызов целен:
События added / дельта-аргумента / done элемента выдаются без изменений, как только вызов проходит.
Оболочка added стримится, затем done, чьи аргументы — отредактированная версия — оригинальные фрагменты дельта-аргумента отбрасываются, так что неотредактированное значение никогда вас не достигает.
Буферизованные события отбрасываются, и отклонённый элемент также отфильтровывается из терминального объекта response.completed, из которого ваш клиент строит своё финальное состояние — нет висячей ссылки на вызов, который никогда не выполнялся.
Текстовые дельты и дельты рассуждения стримятся вживую на всём протяжении, ровно как на chat completions.

4. Native Claude /v1/messages

native-стрим Anthropic — другой зверь: контент приходит как индексированные блокиcontent_block_startcontent_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:
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" нетронутым.
Это применяется к native стримам Claude. Модель Claude, вызванная через эндпоинты формата OpenAI, применяется на проводе 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\"}]}"
}
Стримьте тот же промпт тремя способами, и firewall отклоняет вызов rm -rf каждый раз. Что наблюдает ваш клиент:
ПроводТерминальный сигнал после полного убирания
OpenAI chatfinish_reason: "stop"
OpenAI Responsesэлемент отсутствует в response.completed
Native Claudestop_reason: "end_turn"
Совпавший-и-отклонённый вызов появляется идентично в событиях firewall независимо от провода, так что ваша наблюдаемость независима от провайдера, даже если стрим — нет.

6. Что остаётся постоянным у разных провайдеров

Провод отличается; контракт — нет:
  • Вердикты и правила независимы от провода. allow / audit / deny / sanitize означают одно и то же у каждого провайдера. См. Вердикты.
  • Sanitize трогает только аргументы вызова инструмента, никогда содержимое, которое возвращает инструмент — на каждом проводе. См. Очистку ответов.
  • Allow — это настоящий passthrough. Когда firewall не предпринимает действий, удержанные фреймы воспроизводятся как точные байты upstream — нет пере-батчинга, нет потерянных специфичных для провайдера полей.
  • Shadow-режим применяется везде. Включите его, и удержанные вызовы инструментов всегда выживают (понижены до audit), так что вы можете измерить влияние политики у разных провайдеров до того, как она изменит трафик. См. Shadow-режим.

7. Куда это вписывается

Внутренности стриминга

Механизм удержания-сборки-пересборки, общий для каждого провайдера.

Стадии

Почему применение к стримированным вызовам инструментов живёт на поверхности response.

Вердикты

Независимые от провайдера решения, в которые разрешается стримированный вызов.

Фильтрация response

Ограничение вызовов инструментов, которые выдаёт модель, на стриме или нет.
Об угрозах, которые решают эти стримированные проверки, см. Опасные вызовы инструментов и Эксфильтрацию данных; о том, где применение к стриму сидит на пути запроса, см. Задержку пути применения.