deny ou sanitize sur les appels d’outils que votre modèle émet — et
votre agent appelle la passerelle avec "stream": true. La question qui
compte vraiment : une réponse en streaming peut-elle laisser fuiter un
appel d’outil bloqué avant que le firewall ne décide ? Elle ne le peut pas,
et cette page explique l’unique mécanisme qui rend cela vrai pour que vous
puissiez raisonner sur la latence et les chunks que votre client reçoit.
C’est un regard ciblé sur le comportement SSE. Pour les verdicts eux-mêmes
voir Verdicts ; pour la grammaire des règles
voir la référence des règles.
1. Le problème du firewall sse en streaming
Une réponse non-streaming est un seul corps JSON — le firewall voit le tout, évalue lestool_calls, et renvoie le résultat nettoyé. Un stream est
différent : un modèle émet un appel d’outil comme des dizaines de deltas
tool_call à travers de nombreuses frames SSE, et une fois qu’une frame est
transmise, votre agent l’a déjà — on ne rétracte pas un token qu’on a
envoyé. Évaluez trop tôt et vous n’avez pas l’appel complet (nom + arguments
complets) à juger ; transmettez au fur et à mesure et un deny est déjà trop
tard.
La passerelle résout cela avec un contrat simple et observable :
Le contenu streame en live
Les frames d'appel d'outil sont retenues
tool_call (ou function_call legacy) est
retenue du stream en live jusqu’à ce que l’appel soit complet et
évalué.tool_calls échappé en JSON n’a pas de sous-chaîne littérale à
faire correspondre, donc un raccourci par sous-chaîne transmettrait un appel
d’outil non évalué. Les frames SSE sont petites ; la porte parse chacune
d’elles.2. La séquence retenir-assembler-évaluer
Pour une réponse chat-completions en streaming avec une politique de surface response active, chaque frame que l’amont émet prend l’un de deux chemins :Frame de contenu / role / reasoning / usage → transmise maintenant
Frame de contenu / role / reasoning / usage → transmise maintenant
Frame tool_call (ou function_call legacy) → retenue
Frame tool_call (ou function_call legacy) → retenue
finish_reason de
clôture d’un tour d’outil est retenue à ses côtés, parce que l’émettre tôt
dirait à votre client que le tour est terminé avant que le firewall n’ait
statué.arguments streamés de chaque
appel), évalue chacun contre votre politique sur la surface response — la
même sémantique de verdict et de règle que le chemin non-streaming — et
n’émet que les survivants :
| Verdict de l’appel retenu | Ce que votre client reçoit |
|---|---|
allow / audit | Les frames retenues d’origine, inchangées — une transmission différée, pas un chunk re-batché. |
sanitize | L’appel avec ses arguments réécrits (secrets/PII correspondants remplacés par un token typé), ré-émis. |
deny | L’appel est abandonné. Si c’était le seul appel du tour, le tour se ferme avec finish_reason: "stop" — le stream donne l’impression que le modèle n’a fait aucun appel d’outil. |
3. Un exemple concret
Une politique response avec une règledeny sur *.delete (rédigez-la dans
l’éditeur de règles de la console) et une requête en streaming dont le modèle
décide d’appeler à la fois db.query et db.delete :
db.query — db.delete a été assemblé, évalué, refusé, et
jamais émis. L’appel survivant est ré-indexé depuis 0, et l’événement
firewall pour l’appel refusé atterrit dans votre
journal d’événements avec la règle qui
s’est déclenchée.
4. Les blocks inbound court-circuitent avant que le stream ne commence
La danse des frames retenues n’est que pour la surface response — les appels que le modèle émet. Un denyinbound
(un outil qu’un agent annonce) se déclenche avant l’appel au modèle
amont, de sorte qu’une requête en streaming qui déclenche une règle inbound
n’ouvre jamais de stream SSE du tout : elle renvoie un simple HTTP 400
avec le code d’erreur firewall_blocked, marqué
skip-retry.
Aucune frame, aucune fenêtre de rétention — le block atterrit comme n’importe
quelle erreur non-streaming.
5. Les guardrails sur le même stream
Une réponse en streaming peut porter une politique de sortie de Guardrail et une politique firewall response en même temps. Ils agissent sur des choses différentes — les guardrails filtrent le texte que le modèle streame ; le firewall gouverne les appels d’outils — et ils se composent :- Block de guardrail de sortie (streaming) : le scanner de sortie coupe
le stream au moment où une règle se déclenche, transmet un unique chunk de
remplacement générique —
[Response blocked by content policy.]avecfinish_reason: "content_filter"— et s’arrête. Le message est délibérément générique (pas de catégorie de règle) pour qu’un sondeur ne puisse pas énumérer votre politique. Une rétention firewall en cours quand cela se produit est abandonnée, de sorte qu’un appel d’outil retenu ne peut pas se glisser après le block. - Mask de guardrail de sortie (streaming) : le masquage de la requête avant le modèle est en place ; le masquage in-band en live de la sortie streamée est sur la roadmap. Sur un stream, une règle de mask enregistre la correspondance mais transmet actuellement le chunk d’origine — rédigez-la en sachant que la redaction n’est pas encore réécrite sur le fil. Le block de sortie est pleinement appliqué sur les streams.
6. Ce que cela signifie pour votre client
Quelques conséquences pratiques du modèle des frames retenues :finish_reason peut changer
finish_reason peut changer
finish_reason: "stop" au lieu de "tool_calls" — pour votre agent, ça
se lit comme « le modèle a choisi de ne pas appeler d’outil ». Un tour où
certains appels ont survécu se ferme avec "tool_calls", ne portant que
les survivants.usage arrive quand même
usage arrive quand même
usage de tokens sur le même chunk terminal que
le firewall a retenu, la passerelle le ré-attache à la frame finale
reconstruite — un client qui a demandé l’usage du stream le reçoit quand
même.le texte qui partageait un chunk d'appel d'outil est préservé
le texte qui partageait un chunk d'appel d'outil est préservé
aucun changement de code d'agent
aucun changement de code d'agent
