deny o sanitize sulle chiamate a tool che il tuo modello emette — e il tuo
agent chiama il gateway con "stream": true. La domanda che conta davvero: una
risposta in streaming può far trapelare una chiamata a tool bloccata prima che il
firewall decida? Non può, e questa pagina spiega l’unico meccanismo che lo rende
vero così puoi ragionare sulla latenza e sui chunk che il tuo client riceve.
Questo è uno sguardo focalizzato sul comportamento SSE. Per i verdetti stessi vedi
Verdetti; per la grammatica delle regole vedi il
riferimento delle regole.
1. Il problema del firewall SSE in streaming
Una risposta non in streaming è un singolo corpo JSON — il firewall vede l’intera cosa, valuta itool_calls e restituisce il risultato ripulito. Uno stream è
diverso: un modello emette una chiamata a tool come decine di delta tool_call
attraverso molti frame SSE, e una volta che un frame è inoltrato, il tuo agent ce
l’ha già — non c’è modo di ritrattare un token che hai inviato. Valuta troppo
presto e non hai la chiamata completa (nome + argomenti completi) da giudicare;
inoltra man mano e un deny è già troppo tardi.
Il gateway risolve questo con un contratto semplice e osservabile:
Il contenuto fa stream live
I normali delta di testo e ragionamento passano invariati, in tempo reale —
zero latenza aggiunta sui token che il tuo utente legge.
I frame delle chiamate a tool vengono trattenuti
Qualsiasi frame che porta un delta
tool_call (o il legacy function_call)
viene trattenuto dallo stream live finché la chiamata non è completa e
valutata.Il firewall è un cancello di sicurezza, quindi parsa ogni frame. Non indovina
che un frame sia solo-contenuto dai byte grezzi — un membro
tool_calls con escape
JSON non ha alcuna sottostringa letterale su cui fare match, quindi una scorciatoia
su sottostringa inoltrerebbe una chiamata a tool non valutata. I frame SSE sono
piccoli; il cancello parsa ciascuno.2. La sequenza trattieni-assembla-valuta
Per una risposta chat-completions in streaming con una policy sulla superficie response attiva, ogni frame che l’upstream emette prende uno di due percorsi:Frame di contenuto / ruolo / ragionamento / usage → inoltrato ora
Frame di contenuto / ruolo / ragionamento / usage → inoltrato ora
Scorre fino al tuo client immediatamente, byte per byte. Questi non portano mai
una chiamata a tool, quindi il firewall non ha nulla da decidere.
Frame tool_call (o legacy function_call) → trattenuto
Frame tool_call (o legacy function_call) → trattenuto
Bufferizzato fuori dallo stream live. Il frame di chiusura
finish_reason di un
turno di tool viene trattenuto insieme ad esso, perché emetterlo presto direbbe
al tuo client che il turno è finito prima che il firewall abbia deciso.arguments in stream di ciascuna chiamata),
valuta ognuna rispetto alla tua policy sulla superficie response — la stessa
semantica di verdetto e regole del percorso non in streaming — ed emette solo le
sopravvissute:
| Verdetto della chiamata trattenuta | Cosa riceve il tuo client |
|---|---|
allow / audit | I frame trattenuti originali, invariati — un passaggio ritardato, non un chunk ri-batchato. |
sanitize | La chiamata con i suoi argomenti riscritti (segreti/PII corrisposti sostituiti con un token tipizzato), ri-emessa. |
deny | La chiamata viene scartata. Se era l’unica chiamata del turno, il turno chiude con finish_reason: "stop" — lo stream appare come se il modello non avesse fatto alcuna chiamata a tool. |
3. Un esempio concreto
Una policy di response con una regoladeny su *.delete (scrivila nell’editor
delle regole della console) e una richiesta in streaming il cui modello decide di
chiamare sia db.query sia db.delete:
db.query — db.delete è stato assemblato, valutato, negato e mai emesso. La
chiamata sopravvissuta è ri-indicizzata da 0, e l’evento del firewall per la
chiamata negata atterra nel tuo log degli eventi
con la regola che è scattata.
4. I block inbound fanno short-circuit prima che lo stream inizi
La danza dei frame trattenuti è solo per la superficie response — le chiamate che il modello emette. Un denyinbound (un tool
che un agent pubblicizza) scatta prima della chiamata al modello upstream,
quindi una richiesta in streaming che fa scattare una regola inbound non apre mai
alcuno stream SSE: restituisce un semplice HTTP 400 con codice di errore
firewall_blocked, marcato skip-retry.
Nessun frame, nessuna finestra di hold — il block atterra come ogni errore non in
streaming.
5. Guardrails sullo stesso stream
Una risposta in streaming può portare una policy di output dei Guardrail e una policy di response del firewall in una volta. Agiscono su cose diverse — i guardrails filtrano il testo che il modello fa stream; il firewall governa le chiamate a tool — e si compongono:- Block del guardrail di output (streaming): lo scanner di output taglia lo
stream nel momento in cui una regola scatta, inoltra un singolo chunk di
sostituzione generico —
[Response blocked by content policy.]confinish_reason: "content_filter"— e si ferma. Il messaggio è deliberatamente generico (nessuna categoria di regola) così un prober non può enumerare la tua policy. Un hold del firewall in volo quando questo accade viene scartato, così una chiamata a tool trattenuta non può sgusciare fuori dopo il block. - Mask del guardrail di output (streaming): il mascheramento della richiesta prima che il modello sia attivo; il mascheramento in-band live dell’output in stream è sulla roadmap. Su uno stream una regola di mask registra il match ma attualmente inoltra il chunk originale — scrivila sapendo che la redazione non è ancora riscritta sul filo. Il block di output è pienamente applicato sugli stream.
Questa pagina descrive la forma SSE chat-completions di OpenAI. Lo stesso
contratto trattieni-valuta-emetti è cablato per formato — Anthropic Messages native,
Gemini, xAI e lo stream OpenAI Responses lo portano ciascuno nella propria forma di
evento — così il comportamento osservabile dal cliente è identico indipendentemente
da quale provider ha servito la richiesta.
6. Cosa significa questo per il tuo client
Qualche conseguenza pratica del modello dei frame trattenuti:finish_reason può cambiare
finish_reason può cambiare
Un turno la cui unica chiamata a tool è stata negata chiude con
finish_reason: "stop" invece di "tool_calls" — per il tuo agent si legge
come “il modello ha scelto di non chiamare un tool.” Un turno dove alcune
chiamate sono sopravvissute chiude con "tool_calls", portando solo le
sopravvissute.usage arriva comunque
usage arriva comunque
Quando un upstream raggruppa l’
usage dei token sullo stesso chunk terminale
che il firewall ha trattenuto, il gateway lo ri-attacca al frame finale
ricostruito — un client che ha richiesto l’usage in stream lo ottiene comunque.il testo che condivideva un chunk di chiamata a tool è preservato
il testo che condivideva un chunk di chiamata a tool è preservato
Se il modello ha emesso contenuto e una chiamata a tool nello stesso frame, il
contenuto viene recuperato e ri-emesso anche quando la chiamata a tool viene
rimossa — bloccare una chiamata non scarta mai il tuo testo dell’assistant.
nessuna modifica al codice dell'agent
nessuna modifica al codice dell'agent
Non fai aderire uno stream a nulla di tutto questo. Collega una policy alla
chiave (o imposta un default del workspace) e continua a fare stream esattamente
come prima — l’applicazione è al gateway.
Dove andare dopo
Stage e superfici
inbound, response, mcp, egress — dove valuta ciascuna regola.
Verdetti
allow, audit, deny, sanitize, pending_approval, cap_cost.
Sanitizza gli argomenti
Redige i segreti dagli argomenti di una chiamata a tool — solo a livello di argomenti.
Shadow mode
Declassa i verdetti applicativi ad audit mentre misuri l’impatto.
