Passer au contenu principal
Vous avez activé une règle de surface responsedeny 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 les tool_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 deltas de texte et de raisonnement normaux passent inchangés, en temps réel — zéro latence ajoutée sur les tokens que votre utilisateur lit.

Les frames d'appel d'outil sont retenues

Toute frame portant un delta tool_call (ou function_call legacy) est retenue du stream en live jusqu’à ce que l’appel soit complet et évalué.
Le firewall est une porte de sécurité, donc il parse chaque frame. Il ne devine pas qu’une frame est uniquement du contenu à partir des octets bruts — un membre 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 :
Streame vers votre client immédiatement, octet pour octet. Celles-ci ne portent jamais d’appel d’outil, donc le firewall n’a rien à décider.
Mise en tampon hors du stream en live. La frame 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é.
À la fin du stream, la passerelle assemble les frames retenues en appels d’outils complets (en joignant les fragments arguments streamés de chaque appel), évalue chacun contre votre politique sur la surface responsela 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 retenuCe que votre client reçoit
allow / auditLes frames retenues d’origine, inchangées — une transmission différée, pas un chunk re-batché.
sanitizeL’appel avec ses arguments réécrits (secrets/PII correspondants remplacés par un token typé), ré-émis.
denyL’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.
Si rien ne correspond, vous ne payez que le délai de mise en tampon sur les frames d’appel d’outil — le contenu a déjà streamé en live. Le firewall reconstruit les frames uniquement quand il agit réellement (un deny ou un sanitize) ; un allow propre transmet les octets exacts de votre amont.

3. Un exemple concret

Une politique response avec une règle deny 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 :
SSE timeline (what your agent receives)
───────────────────────────────────────
data: {"choices":[{"delta":{"content":"Looking that up…"}}]}   ← live
data: {"choices":[{"delta":{"content":" one moment."}}]}        ← live
                                                                ← db.query + db.delete
                                                                  tool_call frames HELD
─── end of stream ───
data: {"choices":[{"delta":{"role":"assistant",
        "tool_calls":[{"index":0,"function":{"name":"db.query",…}}]}}]}
data: {"choices":[{"finish_reason":"tool_calls"}]}
Votre agent lit le texte de l’assistant en temps réel, puis reçoit uniquement db.querydb.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.
Déployez d’abord une politique response en streaming sous le mode shadow. En mode shadow, chaque verdict appliquant est rétrogradé en audit (raison préfixée [shadow] would …) et toutes les frames d’appel d’outil passent — de sorte que vous pouvez confirmer que la politique correspond à ce que vous attendez sur du trafic streamé réel avant qu’elle ne commence à abandonner des appels.

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 deny inbound (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.] avec finish_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.
Cette page décrit la forme SSE d’OpenAI chat-completions. Le même contrat retenir-évaluer-émettre est câblé par format — Anthropic Messages natif, Gemini, xAI, et le stream OpenAI Responses le portent chacun dans leur propre forme d’événement — de sorte que le comportement observable par le client est identique quel que soit le fournisseur qui a servi la requête.

6. Ce que cela signifie pour votre client

Quelques conséquences pratiques du modèle des frames retenues :
Un tour dont le seul appel d’outil a été refusé se ferme avec 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.
Quand un amont regroupe l’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.
Si le modèle a émis du contenu et un appel d’outil dans la même frame, le contenu est récupéré et ré-émis même quand l’appel d’outil est retiré — bloquer un appel n’abandonne jamais votre texte d’assistant.
Vous n’inscrivez pas un stream à tout cela. Attachez une politique à la clé (ou définissez un défaut d’espace de travail) et continuez à streamer exactement comme auparavant — l’application est à la passerelle.

Où aller ensuite

Stages et surfaces

inbound, response, mcp, egress — où chaque règle évalue.

Verdicts

allow, audit, deny, sanitize, pending_approval, cap_cost.

Assainir les arguments

Redacter les secrets des arguments d’un appel d’outil — couche des arguments uniquement.

Mode shadow

Rétrograder les verdicts appliquants en audit pendant que vous mesurez l’impact.
Pour savoir où cela se situe sur le chemin de la requête, voir comment OrcaRouter inspecte et la latence du chemin d’application. Pour les menaces que l’application de surface response contient, voir appels d’outils dangereux et exfiltration de données. Pour la grammaire complète des règles, voir la référence des règles du firewall.