response 스테이지가 에이전트가 그것에 따라 행동하기
전에 그것을 벗겨내거나 다시 씁니다. 강제 결정은 모든 프로바이더에서
동일합니다 — 같은 규칙, 같은 판정, 같은 이벤트. 다른 것은 firewall이
스트리밍된 툴 호출에 행동한 후 클라이언트가 보는 와이어 형태입니다.
OpenAI chat, OpenAI Responses API, 그리고 네이티브 Claude /v1/messages가
각각 툴 호출을 다르게 프레이밍하기 때문입니다.
이 페이지는 그 고객 관측 가능한 차이에 대한 집중 노트입니다. 규칙 언어를
다시 문서화하지 않습니다 — Firewall rules
참조 — 스테이지 모델도 마찬가지입니다.
Stages에서 다룹니다. 셋 모두가 공유하는
내부 보류-및-재조립 메커니즘은
스트리밍 내부를 읽으세요.
1. firewall 프로바이더 스트리밍이 와이어별로 다른 이유
비스트리밍 응답에서 firewall은 전체 응답을 한 번에 보고 결정합니다. 스트림에서, 모델의 툴 호출은 조각의 시퀀스로 도착합니다 — 한 프레임의 이름, 더 많은 프레임에 걸쳐 흘러나오는 인자 JSON. 판정은 완전한 호출(이름 과 전체 인자)을 필요로 하며, 툴 호출 조각은 일단 전달되면 철회할 수 없습니다. 그래서 모든 프로바이더에서 게이트웨이는 같은 일을 합니다: 일반 콘텐츠는 라이브로 스트리밍하게 두고, 호출이 완전히 조립될 때까지 툴 호출 프레임을 보류합니다. 스트림 끝에 각 조립된 호출을 평가하고 살아남은 것만 — 그 프로바이더 자체의 이벤트 형태로 — 발행합니다.텍스트는 결코 멈추지 않습니다. 툴 호출 프레임만 보류됩니다. 어시스턴트
콘텐츠, 추론, 그리고 role 프레임은 라이브로 변경 없이 스트리밍됩니다.
보류는 첫 툴 호출 조각부터 그 턴의 끝까지 적용됩니다 — 그래서 채팅 전용
응답은 firewall이 연결되지 않은 것과 정확히 같이 스트리밍됩니다.
2. OpenAI chat completions
/v1/chat/completions에서, 툴 호출은 인덱스로 키가 지정된
delta.tool_calls 조각으로 스트리밍됩니다. 게이트는 그것들(과 레거시
delta.function_call 형태)을 보류하고, 닫는 프레임에서, 살아남은 호출을
0부터 재인덱싱하여 발행하고, 이어서 finish 프레임을 발행합니다:
| 결과 | 클라이언트가 받는 것 |
|---|---|
| allow | 원래 보류된 프레임을, 바이트 단위로 — 진정한 그대로 전달. |
| sanitize | 다시 쓰인 인자를 가진 tool_calls 델타 하나, 그 다음 finish_reason: "tool_calls". |
| deny (일부 호출) | 살아남은 호출만, 그 다음 finish_reason: "tool_calls". |
| deny (모든 호출) | 툴 호출 없음, 그 다음 finish_reason: "stop" — 턴은 모델이 텍스트로 답하기로 선택한 것처럼 보임. |
3. OpenAI Responses API
네이티브/v1/responses 스트림은 자체 이벤트 모델을 가집니다 — 툴 호출은
response.output_item.added로 열리고, response.function_call_arguments.delta
조각을 스트리밍하고, response.output_item.done에서 완료되는
function_call 항목입니다. firewall은 호출이 완전한 첫 지점인 done에서
평가합니다:
allow → 버퍼된 이벤트를 그대로 플러시
allow → 버퍼된 이벤트를 그대로 플러시
항목의
added / 인자 델타 / done 이벤트가 호출이 통과하면 변경 없이
발행됩니다.sanitize → 항목 셸 + 다시 쓰인 done
sanitize → 항목 셸 + 다시 쓰인 done
added 셸이 스트리밍된 다음, 인자가 가려진 버전인 done — 원래 인자
델타 조각은 드롭되므로 가려지지 않은 값이 결코 도달하지 않습니다.deny → 항목이 모든 곳에서 제거됨
deny → 항목이 모든 곳에서 제거됨
버퍼된 이벤트가 드롭되고, 거부된 항목은 클라이언트가 최종 상태를
구축하는 종착
response.completed 객체에서도 필터링됩니다 — 결코
실행되지 않은 호출에 대한 매달린 참조 없음.4. 네이티브 Claude /v1/messages
네이티브 Anthropic 스트림은 다른 짐승입니다: 콘텐츠가 인덱스된 블록으로
도착합니다 — content_block_start → content_block_delta(input_json_delta
조각) → content_block_stop — stop_reason을 담은 message_delta로
닫힙니다. firewall은 첫 tool_use 블록부터 보류하고, 각각을 평가하고,
연속 인덱스로 살아남은 블록을 재구성하므로 벗겨진 블록이 인덱스 갭을
남기지 않습니다.
Claude 특유의 신호는 stop_reason입니다. 모든 tool_use 블록이 거부되면,
tool_use의 stop_reason은 클라이언트에게 결코 도착하지 않는 툴 호출을
약속하게 됩니다 — 그래서 게이트웨이는 그것을 **end_turn**으로 다시
씁니다:
tool_use 블록을 연속적으로 재인덱싱하여
유지하고, stop_reason: "tool_use"를 그대로 둡니다.
5. 하나의 구체적인 예
같은 규칙은 모든 프로바이더에서 같은 결정을 생성합니다 — 클라이언트가 읽는 와이어 형태만 다릅니다.response 스테이지에서 한 번 작성하세요:
rm -rf
호출을 거부합니다. 클라이언트가 관측하는 것:
| 와이어 | 전체 벗겨내기 후 종착 신호 |
|---|---|
| OpenAI chat | finish_reason: "stop" |
| OpenAI Responses | response.completed에서 항목 부재 |
| 네이티브 Claude | stop_reason: "end_turn" |
6. 프로바이더 전반에서 일정하게 유지되는 것
와이어는 다르지만; 계약은 다르지 않습니다:- 판정과 규칙은 와이어 무관입니다.
allow/audit/deny/sanitize는 모든 프로바이더에서 같은 것을 의미합니다. Verdicts 참조. - Sanitize는 툴 호출 인자만 건드립니다, 툴이 반환하는 콘텐츠는 결코 아님 — 모든 와이어에서. 응답 정화 참조.
- Allow은 진정한 그대로 전달입니다. firewall이 아무 행동도 하지 않을 때, 보류된 프레임은 정확한 업스트림 바이트로 재생됩니다 — 다시 배치 없음, 프로바이더 특정 필드 손실 없음.
- Shadow mode는 어디서나 적용됩니다. 그것을 켜면 보류된 툴 호출은
항상 살아남으므로(
audit로 강등), 정책이 트래픽을 변경하기 전에 프로바이더 전반의 영향을 측정할 수 있습니다. Shadow mode 참조.
7. 이것이 어디에 들어맞는가
스트리밍 내부
모든 프로바이더가 공유하는 보류-조립-재조립 메커니즘.
Stages
스트리밍된 툴 호출 강제가 왜
response 표면에 존재하는가.Verdicts
스트리밍된 호출이 해석되는 프로바이더 무관 결정.
Response 필터링
스트림이든 아니든, 모델이 발행하는 툴 호출을 게이트.
