deny o sanitize sobre las llamadas a herramienta que tu modelo emite —
y tu agente llama al gateway con "stream": true. La pregunta que realmente
importa: ¿puede una respuesta en streaming filtrar una llamada a herramienta
bloqueada antes de que el firewall decida? No puede, y esta página explica
el único mecanismo que lo hace verdad para que puedas razonar sobre la
latencia y los chunks que tu cliente recibe.
Esta es una mirada enfocada al comportamiento SSE. Para los veredictos en sí
ver Veredictos; para la gramática de reglas
ver la referencia de reglas.
1. El problema del firewall en streaming SSE
Una respuesta no en streaming es un solo cuerpo JSON — el firewall ve la cosa entera, evalúa lostool_calls, y devuelve el resultado limpio. Un stream
es diferente: un modelo emite una llamada a herramienta como decenas de
deltas tool_call a lo largo de muchos frames SSE, y una vez que un frame
se reenvía, tu agente ya lo tiene — no hay forma de retractar un token que
has enviado. Evalúa demasiado pronto y no tienes la llamada completa (nombre +
argumentos completos) que juzgar; reenvía sobre la marcha y un deny ya es
demasiado tarde.
El gateway resuelve esto con un contrato simple y observable:
El contenido se transmite en vivo
Los deltas normales de texto y razonamiento pasan sin cambios, en
tiempo real — cero latencia añadida en los tokens que tu usuario lee.
Los frames de llamada a herramienta se retienen
Cualquier frame que lleva un delta
tool_call (o el legado
function_call) se retiene del stream en vivo hasta que la llamada
esté completa y evaluada.El firewall es una compuerta de seguridad, así que parsea cada frame. No
adivina que un frame es solo de contenido a partir de los bytes en bruto — un
miembro
tool_calls escapado en JSON no tiene subcadena literal con la que
coincidir, así que un atajo de subcadena reenviaría una llamada a herramienta
sin evaluar. Los frames SSE son pequeños; la compuerta parsea cada uno.2. La secuencia retener-ensamblar-evaluar
Para una respuesta de chat-completions en streaming con una política de superficie response activa, cada frame que el upstream emite toma una de dos rutas:Frame de contenido / role / reasoning / usage → reenviado ahora
Frame de contenido / role / reasoning / usage → reenviado ahora
Se transmite a tu cliente de inmediato, byte a byte. Estos nunca llevan
una llamada a herramienta, así que el firewall no tiene nada que decidir.
Frame tool_call (o el legado function_call) → retenido
Frame tool_call (o el legado function_call) → retenido
Almacenado en buffer fuera del stream en vivo. El frame de cierre
finish_reason de un turno de herramienta se retiene junto a él, porque
emitirlo temprano le diría a tu cliente que el turno terminó antes de que
el firewall haya fallado.arguments transmitidos de
cada llamada), evalúa cada una contra tu política en la superficie response
— la misma semántica de veredicto y regla que la ruta no en streaming — y
emite solo las supervivientes:
| Veredicto de la llamada retenida | Qué recibe tu cliente |
|---|---|
allow / audit | Los frames retenidos originales, sin cambios — un paso retrasado, no un chunk reagrupado. |
sanitize | La llamada con sus argumentos reescritos (secretos/PII coincidentes reemplazados con un token tipado), reemitida. |
deny | La llamada se descarta. Si era la única llamada del turno, el turno cierra con finish_reason: "stop" — el stream parece como si el modelo no hubiera hecho ninguna llamada a herramienta. |
3. Un ejemplo concreto
Una política de response con una regladeny sobre *.delete (autórala en el
editor de reglas de la consola) y una solicitud en streaming cuyo modelo
decide llamar tanto a db.query como a db.delete:
db.query — db.delete fue ensamblada, evaluada, denegada y nunca emitida.
La llamada superviviente se reindexa desde 0, y el evento del firewall para
la llamada denegada aterriza en tu
registro de eventos con la regla que se
disparó.
4. Los bloqueos inbound hacen corto-circuito antes de que el stream empiece
El baile de frames retenidos es solo para la superficie response — llamadas que el modelo emite. Un denyinbound
(una herramienta que un agente anuncia) se dispara antes de la llamada al
modelo upstream, así que una solicitud en streaming que dispara una regla
inbound nunca abre un stream SSE en absoluto: devuelve un HTTP 400 simple
con el código de error firewall_blocked, marcado como
skip-retry.
Sin frames, sin ventana de retención — el bloqueo aterriza como cualquier
error no en streaming.
5. Guardrails en el mismo stream
Una respuesta en streaming puede llevar una política de salida de Guardrail y una política de respuesta del firewall a la vez. Actúan sobre cosas diferentes — los guardrails examinan el texto que el modelo transmite; el firewall gobierna las llamadas a herramienta — y se componen:- Bloqueo de guardrail de salida (streaming): el escáner de salida corta
el stream en el momento en que una regla se dispara, reenvía un único chunk
de reemplazo genérico —
[Response blocked by content policy.]confinish_reason: "content_filter"— y se detiene. El mensaje es deliberadamente genérico (sin categoría de regla) para que un sondeador no pueda enumerar tu política. Una retención del firewall en vuelo cuando esto ocurre se descarta, así que una llamada a herramienta retenida no puede escaparse después del bloqueo. - Enmascaramiento de guardrail de salida (streaming): el enmascaramiento de la solicitud está antes del modelo en vivo; el enmascaramiento en banda en vivo de la salida transmitida está en la hoja de ruta. En un stream una regla de máscara registra la coincidencia pero actualmente reenvía el chunk original — autórala sabiendo que la redacción aún no se reescribe en el cable. El bloqueo de salida está plenamente aplicado en los streams.
Esta página describe la forma SSE de chat-completions de OpenAI. El mismo
contrato retener-evaluar-emitir está cableado por formato — Anthropic Messages
nativo, Gemini, xAI, y el stream de OpenAI Responses lo llevan cada uno en su
propia forma de evento — así que el comportamiento observable por el cliente
es idéntico sin importar qué proveedor atendió la solicitud.
6. Qué significa esto para tu cliente
Algunas consecuencias prácticas del modelo de frames retenidos:finish_reason puede cambiar
finish_reason puede cambiar
Un turno cuya única llamada a herramienta fue denegada cierra con
finish_reason: "stop" en vez de "tool_calls" — para tu agente se lee
como “el modelo eligió no llamar a una herramienta”. Un turno donde
algunas llamadas sobrevivieron cierra con "tool_calls", llevando solo
las supervivientes.usage aún llega
usage aún llega
Cuando un upstream agrupa el
usage de tokens en el mismo chunk terminal
que el firewall retuvo, el gateway lo readjunta al frame reconstruido
final — un cliente que solicitó usage de stream aún lo obtiene.el texto que compartió un chunk de llamada a herramienta se preserva
el texto que compartió un chunk de llamada a herramienta se preserva
Si el modelo emitió contenido y una llamada a herramienta en el mismo
frame, el contenido se recupera y reemite incluso cuando la llamada a
herramienta se quita — bloquear una llamada nunca descarta tu texto de
asistente.
sin cambio de código del agente
sin cambio de código del agente
No optas un stream por nada de esto. Adjunta una política a la clave (o
establece un valor por defecto del espacio de trabajo) y sigue
transmitiendo exactamente como antes — la aplicación está en el gateway.
Dónde ir a continuación
Etapas y superficies
inbound, response, mcp, egress — dónde evalúa cada regla.
Veredictos
allow, audit, deny, sanitize, pending_approval, cap_cost.
Sanear argumentos
Redacta secretos de los argumentos de una llamada a herramienta — solo
capa de argumentos.
Modo shadow
Degrada los veredictos de aplicación a audit mientras mides el impacto.
