Une requête en streaming ne peut être relancée en toute sécurité que si trois conditions sont réunies simultanément : rien n'a atteint votre client, aucune mesure observable n'a été effectuée, et la requête ne comporte aucun état côté serveur. Après le premier événement de sortie, la bonne approche consiste à signaler l'échec plutôt que de relancer la requête.
TokenLab applique cette règle sur sa passerelle pour le streaming de l'API Responses, à la fois via HTTP et WebSocket. Le chemin WebSocket a été modifié le 28/09/2026 pour s'aligner sur le HTTP.
Pourquoi un flux est différent d'une requête normale
Un appel sans streaming renvoie un corps ou une erreur. Vous pouvez relancer la requête en cas d'erreur car vous n'avez rien reçu.
Un flux vous transmet des données avant que la requête ne soit terminée. Le premier événement de sortie est le point de non-retour. Si la connexion est interrompue après cela, vous disposez d'un texte partiel. Relancer la requête signifie générer à nouveau la même réponse et la payer deux fois. Vous risquez également de dupliquer un appel d'outil que votre agent a déjà exécuté.
Le guide de streaming de TokenLab l'indique clairement :
Une fois le premier événement reçu, un flux interrompu est considéré comme incomplet et n'est pas redémarré automatiquement.
Votre client a donc besoin d'un bit d'état local : saw_output. Il passe à « vrai » dès qu'une sortie atteint votre code. Chaque décision de relance lit d'abord ce bit.
Un flux qui se termine sans response.completed est un échec. Ne partez pas du principe que le texte dont vous disposez est complet. Gérez les événements response.failed, response.incomplete et error.
La décision de relance, point par point
TokenLab relance une requête une fois sur une autre route disponible lorsque toutes ces conditions sont remplies : la requête est sans état, rien n'a atteint le client, aucun résultat ou usage n'a été observé pour la tentative échouée, et l'échec est soit un événement pré-sortie relançable, soit une erreur de lecture en amont avant le premier événement. Au maximum une relance est effectuée par requête. Si le remplacement échoue également avant la sortie, cet échec n'est pas relancé.
Source : guide de streaming et comportement de la passerelle TokenLab, observés le 28/09/2026.
| Point de défaillance | Relancé par TokenLab ? | Raison |
|---|---|---|
Événement pré-sortie relançable (response.failed, ou un événement error marqué comme relançable, tel qu'une erreur de surcharge ou une erreur interne en amont) |
Oui, une fois, si la requête est sans état | Rien n'a atteint le client et aucune utilisation n'a été observée, donc une seconde exécution est invisible. |
| Rupture du flux en amont (erreur de lecture) avant le premier événement | Oui, une fois, si la requête est sans état | Même fenêtre. Le client ne détient aucune sortie et aucun coût n'est facturé. |
| Second échec avant sortie, après une relance | Non | Le quota est d'une relance par requête. |
| Tout échec après que la sortie a atteint le client | Non | Le client détient déjà un texte partiel. Une relance dupliquerait la sortie et le coût. |
Réponse stockée (store), continuation (previous_response_id) ou requête liée à une origine |
Non | Une seconde exécution pourrait créer une seconde réponse stockée ou diverger l'état de la conversation. |
| Expiration du délai du premier événement | Non | L'amont peut toujours être en train de générer. Une relance pourrait effectuer le même travail deux fois pendant que la première tentative se poursuit. |
| Dépassement de tampon pré-sortie | Non | La limite est locale à la passerelle. Le même préfixe trop volumineux l'atteindrait très probablement à nouveau sur la route suivante. |
| Client déconnecté | Non | Le client a cessé d'écouter. |
| Échec déterministe, par exemple une requête invalide | Non | La relance ne peut pas changer le résultat. Livré tel quel. |
| Échec ayant déjà entraîné une utilisation | Non | La tentative a été comptabilisée. Livré tel quel. |
| Aucune autre route disponible | Non | Il n'y a nulle part où l'envoyer. Le client reçoit l'échec avec son propre code. |
Lorsqu'un échec n'est pas relancé, ou qu'aucune autre route n'est disponible, vous le recevez avec son propre code d'erreur. Exemples publics : stream_read_error lorsque le flux en amont est rompu, et upstream_stream_buffer_limit en cas de dépassement de tampon. Si la sélection de route elle-même échoue après une décision de relance, le tour WebSocket se termine avec websocket_response_failed (statut 500) et la charge réservée est remboursée.
La facturation suit la même logique. Vous ne payez que pour la tentative livrée. Une requête relancée peut avoir été exécutée deux fois en amont, et ce coût supplémentaire en amont est à la charge de TokenLab, car rien ne vous est parvenu de la première tentative. Un tour échoué qui ne livre rien est remboursé.
Un détail de timing est important pour votre gestion des erreurs. Avant que la sortie ne commence, la passerelle conserve response.created et response.in_progress jusqu'au premier événement de sortie ou à l'arrivée d'un échec, pendant au maximum 10 secondes. Ces événements conservés vous parviennent ensuite avec la première sortie, ou avec l'événement terminal. L'ordre et le contenu restent inchangés. Vous les voyez simplement un peu plus tard. Ces 10 secondes sont un maximum, pas un délai typique.
Ce qui a changé pour WebSocket le 28/09/2026
TokenLab sert l'API Responses via le streaming HTTP ("stream": true, événements envoyés par le serveur) et via WebSocket sur wss://api.tokenlab.sh/v1/responses, où le client envoie des événements response.create. Les réponses WebSocket sont toujours en streaming. Elles ne prennent pas en charge background ou response.cancel. Chaque connexion gère une réponse active à la fois pendant une durée maximale de 60 minutes.
Avant ce changement, les deux chemins divergeaient. Le HTTP conservait les événements de cycle de vie et relançait les échecs pré-sortie sans état. Le WebSocket transmettait response.created immédiatement et livrait les échecs pré-sortie au client, en les remboursant. Le même incident en amont produisait une réponse propre en HTTP et une erreur en WebSocket.
Le chemin WebSocket suit désormais la règle HTTP, y compris la relance d'un flux qui se rompt avant l'arrivée de tout événement. En interne, la plupart des échecs en amont observés sur les tours WebSocket se produisaient avant toute sortie. C'est exactement la fenêtre où une relance est sûre.
La passerelle améliore le cas de l'échec pré-sortie. Elle ne garantit pas qu'un flux se termine.
Comment le changement a été déployé sans casser d'autres comportements
Le travail a suivi un processus conçu pour détecter les changements de comportement silencieux.
- Verrouillage du comportement. Avant le changement, chaque scénario de tour WebSocket était enregistré sous forme de fixture : les trames reçues par le client, les appels en amont effectués et le résultat de la facturation. La suite a atteint 63 scénarios enregistrés au cours de ce travail. Un changement de comportement doit être déclaré à l'avance. Seules les fixtures nommées dans cette déclaration peuvent changer. Toute autre fixture doit rester identique octet par octet.
- Vérifications de mutation. Chaque nouvelle règle de décision a été testée en l'inversant délibérément, comme relancer une expiration de délai du premier événement ou ne pas relancer une erreur de lecture, et en confirmant que le verrouillage échoue.
- Une vérification de revue. La première version rendait également le cas de dépassement de tampon relançable, en prétendant à une parité avec le HTTP. La revue a montré que le HTTP ne relance jamais ce cas, pour la raison indiquée dans le tableau. Une mise à jour a restauré l'ancien comportement et ajouté des scénarios limites : un second échec de lecture n'est pas relancé, aucune route restante, un échec après un
response.createdconservé, et un flux de remplacement qui se rompt ensuite.
Les journaux de requête d'un tour ayant réussi après une relance enregistrent désormais également la tentative échouée précédente, comme le faisait déjà le HTTP.
Code client qui gère la décision de relance
Réglez les relances automatiques du SDK sur 0 pour les appels en streaming. Cela maintient la décision dans votre code. Gardez la décision de relance à un seul endroit, pas dispersée dans les gestionnaires. Pour les erreurs HTTP, respectez retryable et retry_after comme décrit dans le guide de gestion des erreurs, et conservez les identifiants de requête.
SSE via HTTP
import os
from openai import OpenAI
with OpenAI(
api_key=os.environ["TOKENLAB_API_KEY"],
base_url="https://api.tokenlab.sh/v1",
timeout=30.0,
max_retries=0, # gérez la décision de relance au lieu de renvoyer un flux à moitié lu
) as client:
completed, saw_output = False, False
with client.responses.create(
model="gpt-5.6-terra",
input="Répondez par une courte phrase sur les relances.",
stream=True,
) as stream:
for event in stream:
if event.type == "response.output_text.delta":
saw_output = True
print(event.delta, end="", flush=True)
elif event.type == "response.completed":
completed = True
elif event.type in {"response.failed", "response.incomplete", "error"}:
raise RuntimeError(f"{event.type} after_output={saw_output}")
if not completed:
raise RuntimeError(f"flux fermé avant response.completed, after_output={saw_output}")
print()
L'exemple utilise le SDK OpenAI 2.15.0 avec https://api.tokenlab.sh/v1 et max_retries=0. Il suit saw_output et déclenche une erreur sur les événements response.failed, response.incomplete et error, ainsi que sur un flux qui se ferme avant response.completed. Vérifié en production le 28/09/2026 avec gpt-5.6-terra.
Si l'échec arrive avec saw_output == false et que la requête était éligible (sans état, avec un échec relançable), TokenLab l'a déjà relancée une fois ; les réponses stockées, les continuations et les expirations de délai du premier événement n'ont pas été relancées du tout. Décidez au niveau de l'application si une nouvelle requête est acceptable, car une nouvelle requête est une nouvelle génération. Si saw_output == true, signalez l'échec et montrez ce que vous avez, ou rejetez délibérément le texte partiel.
WebSocket
import asyncio
import json
import os
import websockets
URL = "wss://api.tokenlab.sh/v1/responses"
TERMINAL = {"response.completed", "response.failed", "response.incomplete", "error"}
async def run_turn(prompt: str) -> str:
headers = {"Authorization": f"Bearer {os.environ['TOKENLAB_API_KEY']}"}
async with websockets.connect(URL, additional_headers=headers, max_size=None) as ws:
await ws.send(json.dumps({
"type": "response.create",
"model": "gpt-5.6-terra",
"input": prompt,
"store": False,
}))
text, saw_output = [], False
async for raw in ws:
event = json.loads(raw)
kind = event.get("type")
if kind == "response.output_text.delta":
saw_output = True
text.append(event["delta"])
elif kind in TERMINAL:
if kind != "response.completed":
# Une fois la sortie commencée, un échec est définitif pour ce tour.
# Ne renvoyez que si votre application peut rejeter le texte partiel.
raise RuntimeError(f"{kind} after_output={saw_output}: {json.dumps(event)[:300]}")
return "".join(text)
raise RuntimeError(f"socket fermé avant un événement terminal, after_output={saw_output}")
print(asyncio.run(run_turn("Répondez par une courte phrase sur les relances.")))
L'exemple utilise websockets 16.0, se connecte à wss://api.tokenlab.sh/v1/responses avec un en-tête Bearer, envoie un response.create avec store: false, et collecte response.output_text.delta. Il déclenche une erreur avec after_output sur tout événement terminal non complété ou une fermeture précoce. Vérifié en production le 28/09/2026 avec gpt-5.6-terra.
Le drapeau after_output repose sur la même idée que saw_output. Il indique à votre code appelant si un nouveau tour est possible sans dupliquer les effets secondaires.
Liste de contrôle pour votre propre logique de relance
- Traitez un flux qui se termine sans
response.completedcomme un échec, à chaque fois. - Suivez un booléen indiquant si la sortie a atteint votre code. Activez-le lors du premier événement de sortie, pas lors du premier événement de cycle de vie.
- Un échec pré-sortie sur une requête éligible a déjà bénéficié de sa relance unique par la passerelle ; une tentative supplémentaire est votre décision.
- Après une sortie partielle, ne renvoyez que si votre application peut rejeter le texte partiel et accepter de payer pour deux générations.
- Dans les boucles d'agents, vérifiez si le flux partiel contenait déjà un appel d'outil sur lequel votre code a agi. Ne relancez pas un tour dont vous ne pouvez pas annuler les effets secondaires.
- Pour les réponses stockées et les continuations
previous_response_id, inspectez l'état existant avant de renvoyer quoi que ce soit. - Réglez les relances de streaming sur 0 dans votre SDK et gardez la décision de relance dans une seule fonction.
- Enregistrez les identifiants de requête pour pouvoir faire correspondre une réponse livrée aux tentatives qui l'ont précédée.
FAQ
TokenLab redémarre-t-il un flux après une sortie partielle ?
Non. Une fois que la sortie a atteint votre client, un échec est signalé et n'est jamais relancé. Vous disposez d'un texte partiel, donc un redémarrage dupliquerait la sortie et le coût. Votre application décide s'il faut afficher, tronquer ou rejeter ce qu'elle a reçu.
Serai-je facturé deux fois si la passerelle relance ma requête ?
Non. Vous ne payez que pour la tentative livrée. Une requête relancée peut avoir été exécutée deux fois en amont, mais rien ne vous est parvenu de la première tentative, et ce coût supplémentaire en amont est à la charge de TokenLab. Un tour échoué qui ne livre rien est remboursé.
Pourquoi une expiration de délai du premier événement n'est-elle pas relancée ?
Parce que l'amont peut toujours être en train de générer. Une relance pourrait effectuer le même travail deux fois pendant que la première tentative se poursuit. Une expiration de délai du premier événement est traitée différemment d'une erreur de lecture qui rompt le flux avant le premier événement.
Puis-je relancer une réponse stockée ou une continuation previous_response_id ?
Pas automatiquement. TokenLab ne relance jamais les réponses stockées, les continuations ou les requêtes liées à une origine, car une seconde exécution pourrait créer une seconde réponse stockée ou diverger l'état de la conversation. Vérifiez l'état existant avant de renvoyer, et ne renvoyez que si votre application peut réconcilier cet état.
Si vous souhaitez surveiller vous-même le flux d'événements brut, créez une clé API et enregistrez chaque type d'événement reçu par votre client. Le guide de streaming et le guide de gestion des erreurs couvrent l'ensemble des événements. Pour en savoir plus sur la façon dont la passerelle route et récupère les données, consultez l'infrastructure de fiabilité de l'API TokenLab AI et l'API Responses vs Chat Completions pour les agents.
Sources
- https://docs.tokenlab.sh/guides/streamingObservé le 2026-09-28
- https://docs.tokenlab.sh/guides/error-handlingObservé le 2026-09-28



