Interruptions de streaming et SSE : récupération sans doublons

Distinguez les coupures SSE, conservez la sortie partielle et relancez sans dupliquer les effets de bord.

Streaming et SSE interrompus : récupérer sans doublons

Une requête d'API en streaming n'est terminée qu'après l'event terminal prévu par son protocole. Un socket fermé, un timeout côté client ou le dernier fragment de texte ne prouvent pas la fin de l'opération. Après une coupure, conservez les events déjà reçus, la Request ID et l'état de l'opération. Ne relancez qu'après avoir vérifié les effets de bord : il n'existe pas de reprise universelle depuis le dernier token et un retry aveugle peut exécuter un outil deux fois.

Fin normale ou véritable coupure

Les Server-Sent Events transmettent une suite d'events sur une connexion HTTP longue. Le client les lit jusqu'à l'un de ces résultats :

created -> receiving -> completed \-> failed \-> disconnected

completed est l'event terminal qui confirme le succès du protocole. failed est une erreur transmise à l'intérieur du stream. disconnected indique que le transport s'est terminé sans fin confirmée ; ce cas exige un diagnostic.

Dans l'API OpenAI Responses, on rencontre des types tels que response.created, des fragments d'output et response.completed, ainsi que des états d'échec ou incomplets. Anthropic Messages utilise message_start, les events de bloc de contenu, message_delta et message_stop. Ne mélangez pas ces noms dans un même parser.

Vous voulez reproduire une coupure de streaming avec une requête contrôlée ? Créez votre propre compte BetterToken et API Key, ouvrez la référence API et commencez par un stream court sans Tool Calls. Comparez ensuite l'heure, le modèle et le statut avec l'entrée du Dashboard. N'ajoutez un retry limité qu'après avoir vérifié l'event terminal et la sortie partielle.

Données à conserver lors d'une coupure

Un journal minimal permet de distinguer un problème client d'un problème serveur :

{ "started_at": "2026-08-03T12:00:00Z", "protocol": "openai-responses", "request_id": "req_placeholder", "http_status": 200, "last_event_type": "response.output_text.delta", "events_received": 42, "bytes_received": 8192, "terminal_event_received": false, "client_error": "socket closed" }

N'enregistrez ni l'API Key, ni le prompt complet, ni les arguments d'outil, ni une réponse sensible. Ne conservez le texte partiel que si la politique de l'application l'autorise. En production, le hash de l'opération, le nombre d'events et le dernier marqueur de séquence sûr sont souvent plus utiles qu'une archive complète du payload.

La Request ID peut venir des en-têtes HTTP ou d'un event. Enregistrez-la dès que possible, et non après la fin du stream, afin de pouvoir rapprocher les journaux du client, du proxy et de la requête observée.

Vérifier le parser avant le réseau

Le client doit traiter correctement :

  • plusieurs lignes data: dans un même event ;
  • les lignes vides entre les events ;
  • les fragments UTF-8 à la frontière des chunks réseau ;
  • un type d'event inconnu sans faire tomber le processus ;
  • un event d'erreur après un HTTP status réussi ;
  • un event terminal sans le dernier delta de texte attendu ;
  • des arguments d'outil répartis sur plusieurs fragments.

Un chunk TCP n'est pas un event SSE. Un event peut arriver en plusieurs parties, et plusieurs events peuvent être lus d'un coup. Assemblez d'abord une frame SSE complète, puis parsez son JSON.

Pseudocode du handler :

state = receiving for event in parse_sse(response_body): log_safe_metadata(event) apply_event_to_partial_result(event) if is_terminal_success(event): state = completed break if is_terminal_failure(event): state = failed break if connection_closed and state == receiving: state = disconnected

Les fonctions is_terminal_success et is_terminal_failure doivent être distinctes pour Responses, Chat Completions et Messages. Un parser qui assimile un event inconnu à un succès masque l'information nécessaire pour décider d'un retry.

Vérifier le timeout à chaque couche

Une connexion longue traverse plusieurs timers :

  1. timeout du SDK ou du client HTTP ;
  2. timeout idle/read de l'application ;
  3. reverse proxy ;
  4. load balancer ou ingress ;
  5. proxy d'entreprise ;
  6. réseau mobile ou domestique ;
  7. limite de génération côté serveur.

Le timeout général de requête et le timeout d'inactivité sont deux paramètres différents. Si le modèle envoie régulièrement des events, un délai d'inactivité court ne devrait pas expirer. Si de longues pauses entre events sont admises, la valeur doit correspondre à la charge attendue.

Vérifiez aussi le buffering du proxy. Un proxy qui accumule les chunks SSE laisse l'utilisateur sans texte longtemps, puis livre un gros bloc ou provoque un timeout. Mesurez le temps jusqu'au premier event et les intervalles entre events localement, derrière le reverse proxy et en production.

Sortie partielle : afficher, conserver ou supprimer

Le texte partiel est utile dans l'interface, mais son état doit être explicite. N'affichez jamais une réponse tronquée comme terminée.

Un modèle d'état pratique est :

  • streaming : le texte évolue encore ;
  • complete : l'event terminal de succès est arrivé ;
  • partial : la connexion a été perdue après plusieurs events ;
  • failed : le protocole a envoyé une erreur ;
  • cancelled : l'utilisateur ou l'application a arrêté la requête.

Pour partial, stockez le texte reçu séparément de toute nouvelle génération. Coller automatiquement deux générations est dangereux : le modèle peut répéter une partie de la réponse, changer la formulation ou appeler des outils dans un autre ordre. L'interface peut montrer l'ancien résultat comme incomplet et la nouvelle génération séparément ; un remplacement doit être explicite.

Quand une requête peut être répétée

La sûreté d'un retry dépend de l'action exécutée.

Texte sans action externe

Une requête textuelle courte peut généralement être répétée avec un nombre d'essais limité. L'application affiche l'ancien résultat comme partial et le nouveau comme une génération distincte, ou le remplace après confirmation explicite. La dernière phrase visible n'est pas une preuve de complétude.

Tool Calls et transactions

Avant de relancer, vérifiez si l'outil a déjà été exécuté. Si le stream s'interrompt après l'envoi de la commande, une seconde requête peut recréer une issue, un e-mail ou une transaction de paiement. Utilisez un Idempotency Key au niveau de l'outil, votre propre Operation ID et un journal des actions terminées. Un socket fermé ne suffit pas à justifier une nouvelle exécution.

Tâche d'agent longue

La reprise automatique « depuis le dernier token » est rarement confirmée par le protocole. Restaurez plutôt la tâche à partir de l'état applicatif sauvegardé : messages confirmés, résultats d'outils et dernière étape terminée. Des fragments de texte bruts ne constituent pas un état d'agent cohérent.

Retry limité avec backoff

Une politique de retry doit avoir un nombre fini de tentatives :

attempts = 0 while attempts < MAX_ATTEMPTS: result = run_request(operation_id) if result.completed: return result if not result.retryable: raise result.error wait(base_delay * 2**attempts + random_jitter) attempts += 1

Le caractère retryable dépend de l'erreur de protocole, du HTTP status, de la présence d'un event terminal et des effets de bord. 401, un mauvais Model ID et un JSON invalide ne seront pas corrigés par une attente. 429, un 5xx temporaire ou une panne de transport peuvent parfois être relancés, mais seulement avec une limite et en tenant compte des en-têtes du provider.

Le backoff réduit la pression lors d'un incident temporaire, mais ne remplace pas la classification de l'erreur. Gardez la même Operation ID pour le rapprochement, sans en déduire qu'un outil externe déjà déclenché peut être répété sans risque.

Test minimal

  1. Envoyez une requête de streaming courte sans outils.
  2. Notez tous les types d'event et attendez l'event terminal.
  3. Coupez artificiellement le client après quelques events.
  4. Vérifiez que le résultat est marqué partial.
  5. Testez un seul retry limité.
  6. Répétez derrière le proxy de production.
  7. Comparez les deux requêtes par heure, modèle et statut dans le Dashboard.

Prenez les types d'event exacts dans la documentation officielle : OpenAI Streaming Responses et Anthropic Messages Streaming.

FAQ

Peut-on reprendre un stream depuis le dernier token ?

Il n'existe pas de mécanisme universel. Conservez le résultat partiel et l'état de l'application, puis suivez les capacités de l'API concernée. Une nouvelle requête peut répéter ou modifier le texte.

Pourquoi le stream finit-il en erreur avec un HTTP status 200 ?

Les en-têtes arrivent avant la génération complète. L'erreur peut apparaître ensuite sous forme d'event de protocole ou de coupure de transport. Le status seul ne suffit donc pas.

Faut-il répéter après chaque déconnexion ?

Non. Vérifiez d'abord l'event terminal, l'erreur, la Request ID et les effets de bord. Un retry pour un Tool Call exige l'idempotence.

Où chercher si tout fonctionne en local ?

Examinez le reverse proxy, le load balancer, le timeout idle, le buffering et le réseau d'entreprise. Comparez les intervalles entre events avant et après chaque couche, au lieu de vous fier à un seul essai local.

Que vérifier dans BetterToken ?

Ouvrez le Dashboard et rapprochez l'heure, le modèle, le statut et l'usage. Vérifiez les paramètres actuels dans la référence API ; n'envoyez jamais au support l'API Key complet ni un prompt sensible.

Prêt à optimiser votre workflow LLM ?

Connectez vos modèles via une API unique, gérez les clés et maîtrisez vos dépenses d’IA.