Guides

Streaming

Comment les réponses en streaming sont transmises, ce que contient chaque chunk et comment lire un stream.

Afficher en Markdown

Certains endpoints peuvent envoyer leur résultat pendant sa production au lieu de l'envoyer en une seule fois : une réponse du chat lorsque la requête définit "stream": true, et une exécution de workflow. Tous les streams utilisent la même enveloppe, décrite ici ; ce que transporte l'enveloppe dépend de l'endpoint. Cette page décrit l'enveloppe et le stream du chat ; les événements d'une exécution de workflow sont décrits avec l'endpoint de workflow.

Comment un stream est transmis

Une réponse en streaming est une réponse HTTP ordinaire dont le corps arrive morceau par morceau :

  • Statut 200, Content-Type: text/event-stream; charset=utf-8, encodage de transfert chunked (pas de Content-Length).
  • Le corps est une suite de chunks. Chaque chunk est un objet JSON, et les objets sont écrits directement les uns après les autres, sans séparateur : pas de saut de ligne, pas de préfixe data:.
text
{"timestamp": 1759406400810, "data": "Unit tests", "error": null}{"timestamp": 1759406400834, "data": " catch", "error": null}{"timestamp": 1759406400851, "data": " regressions", "error": null}

Malgré le type de contenu, le corps n'est pas au format Server-Sent Events, si bien que EventSource et les bibliothèques clientes SSE ne peuvent pas le lire (et EventSource ne peut de toute façon pas envoyer un POST avec un corps). Lisez le corps comme un flux d'octets et découpez-le vous-même en objets JSON, voir Lire un stream. Une lecture réseau peut s'arrêter au milieu d'un objet ou contenir plusieurs objets ; utilisez donc toujours un tampon.

Les erreurs détectées avant le début du stream (authentification, autorisations, requête invalide, modèle inconnu, ...) sont renvoyées comme une réponse d'erreur normale avec leur propre statut HTTP et un corps {"detail": "..."}, voir Erreurs. Vérifiez le code de statut avant de lire le corps comme un stream.

Chunks

Champ Type Description
timestamp horodatage Moment où le chunk a été produit (millisecondes depuis l'epoch).
data chaîne, objet ou null Le payload. Sa forme dépend de l'endpoint et, pour le chat, de runner_version. null dans un chunk d'erreur.
error Erreur de stream ou null Défini lorsque la requête a échoué après le début du stream ; null sinon.

Keepalive. Lorsque rien n'a été produit pendant environ une seconde (le modèle réfléchit, un outil s'exécute), le serveur envoie un chunk dont data vaut la chaîne vide "". Ignorez ces chunks ; ils servent uniquement à éviter que la connexion et les éventuels proxys n'expirent. Les keepalives sont envoyés dans tous les formats de stream, si bien que data peut valoir "" même dans les streams dont les payloads sont par ailleurs des objets.

Erreurs dans un stream

Une fois le stream commencé, le statut HTTP est déjà 200. Un échec survenant après ce point est envoyé comme dernier chunk avec error défini, puis le stream se termine.

json
{"timestamp": 1759406402117, "data": null, "error": {"status": 422, "text": "Validation error", "detail": "not enough user credit"}}
Champ Type Description
status entier Le statut HTTP qu'aurait eu l'erreur dans une réponse normale, par exemple 422 ou 500.
text chaîne Nom court de la classe d'erreur : Validation error, Forbidden, Not found, Unauthorized, Too many requests, Lock error, Internal server error ; Internal Server Error pour les défaillances inattendues.
detail chaîne Le message d'erreur, le même texte que celui que porte detail dans une réponse d'erreur normale.

Les erreurs du chat qui peuvent arriver de cette façon sont marquées pendant dans le tableau des erreurs du chat ; la plus fréquente est l'épuisement des crédits au milieu d'une réponse. Une réponse du chat interrompue par une erreur est conservée dans la conversation jusqu'au point où elle s'est arrêtée.

Fin du stream

Le stream se termine lorsque le corps de la réponse HTTP se termine. Il n'y a pas de chunk final de « fin » :

  • le corps s'est terminé après un chunk avec error défini : la requête a échoué ;
  • le corps s'est terminé autrement : la requête a abouti.

Dans le stream d'événements du chat, un événement done marque le moment où le modèle a terminé sa réponse, mais considérez la fin du corps comme la fin du stream.

Si votre client ferme la connexion prématurément, le serveur arrête de générer la réponse du chat. La partie générée jusque-là est conservée dans la conversation et les appels au modèle déjà effectués sont facturés. (Une exécution de workflow continue sur le serveur après une déconnexion.)

Formats du stream de chat

Le champ runner_version de la requête de chat détermine ce que contient data dans un stream de chat :

runner_version data À utiliser lorsque
"1" (par défaut) Une chaîne : le morceau suivant du texte de la réponse. Vous n'avez besoin que du texte de la réponse.
"2" Un objet événement : texte, raisonnement, activité des outils, statut. Vous voulez afficher la progression, le raisonnement ou l'activité des outils séparément du texte.

Les deux formats proviennent de la même génération et la réponse stockée est identique ; seul ce qui est transmis sur le réseau diffère.

Un modèle sans la capacité stream (voir Modèles et outils) peut quand même être utilisé en streaming : le texte de sa réponse arrive en un seul morceau (un fragment de texte, ou un événement text) lorsqu'il est complet, et il est stocké comme toute autre réponse.

Stream texte (version 1)

Chaque data est un fragment de la réponse au format Markdown. Concaténez les fragments dans l'ordre pour obtenir la réponse complète ; les chunks keepalive ("") n'ajoutent rien.

Les appels d'outils font partie du texte, exactement comme dans le content d'une réponse non streamée : chaque appel d'outil est un bloc qui commence par le marqueur "\n\n### calling AI tool ...\n", se poursuit avec le texte de progression que rapporte l'outil et se termine par le marqueur "\n***\n". Voir Appels d'outils dans la réponse. Le raisonnement, les informations de statut et les étapes internes des sous-assistants ne sont pas envoyés dans ce format.

text
{"timestamp": 1759406400120, "data": "", "error": null}{"timestamp": 1759406400810, "data": "Let me check", "error": null}{"timestamp": 1759406400833, "data": " that.", "error": null}{"timestamp": 1759406401002, "data": "\n\n### calling AI tool ...\n", "error": null}{"timestamp": 1759406401004, "data": "### Scraping URL: https://example.com/rates\n", "error": null}{"timestamp": 1759406402950, "data": "\n***\n", "error": null}{"timestamp": 1759406403410, "data": "The current rate is 24.35 CZK per euro.", "error": null}

Stream d'événements (version 2)

Chaque data (hors keepalives) est un objet événement. Tous les champs sont toujours présents ; ceux qui ne s'appliquent pas au type d'événement sont vides ("", null ou {}).

Champ Type Description
type chaîne Type d'événement, voir Types d'événements.
delta chaîne Texte porté par les événements text, reasoning et tool_call_output ; "" sinon.
phase chaîne ou null Phase d'un événement status, voir Phases de statut.
call_id chaîne ou null Identifiant de l'appel d'outil auquel appartient l'événement (événements d'outil, nested, certains événements status).
tool_name chaîne ou null Nom technique de l'outil, le même identifiant que celui que liste Modèles et outils et que référencent les assistants.
tool_display_name chaîne ou null Nom lisible de l'outil.
tool_icon chaîne ou null Identifiant de l'icône que l'application AYETO utilise pour l'outil ; peut être vide.
tool_status chaîne ou null Résultat dans tool_call_end : ok ou error.
data objet Valeurs supplémentaires de certains événements (statut model_selected, done). {} sinon.
chunk événement ou null L'événement interne d'un événement nested.

Un chunk complet du stream d'événements ressemble à ceci :

json
{"timestamp": 1759406400810, "data": {"type": "text", "delta": "Unit tests", "phase": null, "call_id": null, "tool_name": null, "tool_display_name": null, "tool_icon": null, "tool_status": null, "data": {}, "chunk": null}, "error": null}

Types d'événements

type Signification Champs utilisés
text Morceau suivant du texte visible de la réponse (Markdown). delta
reasoning Morceau suivant du raisonnement du modèle ou du résumé de sa réflexion, pour les modèles qui l'exposent. Ne fait pas partie du texte de la réponse. delta
status La requête est entrée dans une nouvelle phase. phase, et selon la phase call_id, tool_name, tool_display_name, tool_icon, data
tool_call_start Un outil a commencé à s'exécuter. call_id, tool_name, tool_display_name, tool_icon
tool_call_output Texte de progression rapporté par l'outil en cours d'exécution (Markdown), par exemple des requêtes de recherche ou un lien vers un fichier généré. call_id, delta
tool_call_end L'outil a terminé. call_id, tool_status
nested Un événement d'un sous-assistant que l'assistant a appelé comme outil. call_id est l'identifiant de cet appel d'outil, chunk est l'événement propre du sous-assistant (de n'importe quel type, y compris de nouveau nested pour les niveaux plus profonds). call_id, chunk
done Le modèle a terminé sa réponse. data.stop_reason est la raison donnée par le fournisseur, par exemple stop, end_turn ou completed, ou null. data

Phases de statut

phase Signification
context_building La requête est en cours de préparation : instructions, outils, historique. Généralement le premier événement.
rag_query La base de connaissances de l'assistant est en cours de recherche.
relevant_history La partie pertinente de l'historique est en cours de sélection (relevant_history dans la requête).
model_selected Nomme le modèle qui répond : data.model (identifiant du modèle), data.model_name (nom affiché) et, lorsque l'assistant utilise un modèle automatique, data.auto_model (identifiant du modèle automatique qui l'a choisi).
model_call Une requête a été envoyée au modèle et ses premiers tokens sont attendus. Renvoyé après chaque série d'appels d'outils.
context_compacted L'historique de la conversation a été raccourci pour tenir dans la fenêtre de contexte du modèle.
tool_call_pending Le modèle est en train d'écrire un appel d'outil ; call_id et tool_name l'identifient. L'outil démarre avec tool_call_start.
silent_tool Un outil sans sortie visible est en cours d'exécution ; call_id, tool_name, tool_display_name et tool_icon l'identifient. Aucun événement tool_call_* ne suit pour lui.

De nouveaux types d'événements, phases et clés de data peuvent être ajoutés. Ignorez les valeurs que vous ne connaissez pas.

Ordre des événements

Une réponse typique qui utilise un outil produit (keepalives omis, un data par ligne) :

json
{"type": "status", "phase": "context_building", ...}
{"type": "status", "phase": "model_selected", "data": {"model": "gpt-5-mini", "model_name": "GPT-5 mini"}, ...}
{"type": "status", "phase": "model_call", ...}
{"type": "text", "delta": "Let me check", ...}
{"type": "text", "delta": " that.", ...}
{"type": "status", "phase": "tool_call_pending", "call_id": "call_9f2c", "tool_name": "WebScraperFunctionTool", ...}
{"type": "tool_call_start", "call_id": "call_9f2c", "tool_name": "WebScraperFunctionTool", "tool_display_name": "Web Scraper", "tool_icon": "ayeto", ...}
{"type": "tool_call_output", "call_id": "call_9f2c", "delta": "### Scraping URL: https://example.com/rates\n", ...}
{"type": "tool_call_end", "call_id": "call_9f2c", "tool_status": "ok", ...}
{"type": "status", "phase": "model_call", ...}
{"type": "text", "delta": "The current rate is 24.35 CZK per euro.", ...}
{"type": "done", "data": {"stop_reason": "stop"}, ...}

Reconstituer le texte de la réponse

Pour obtenir le même texte que celui que contient un stream texte ou le message stocké, ajoutez pour chaque événement de premier niveau :

Événement Ajouter
text delta
tool_call_start "\n\n### calling AI tool ...\n"
tool_call_output delta
tool_call_end "\n***\n"
tout autre rien

Les fichiers générés (par exemple des images) apparaissent sous forme de liens Markdown dans tool_call_output. Une fois le stream terminé, vous pouvez lire la réponse stockée, y compris ses pièces jointes, via l'API Conversations.

Lire un stream

Les exemples envoient un message de chat avec runner_version "2", affichent le texte de la réponse au fur et à mesure de son arrivée et s'arrêtent sur un chunk d'erreur. Tous deux découpent le corps en objets JSON à l'aide d'un petit tampon, si bien qu'ils fonctionnent quelle que soit la façon dont le réseau découpe les données.

Python

python
import codecs
import json
import os
import uuid

import requests


def iter_chunks(response):
    """Yield the JSON chunks of a streamed response."""
    decoder = json.JSONDecoder()
    text = codecs.getincrementaldecoder("utf-8")()
    buffer = ""
    for raw in response.iter_content(chunk_size=None):
        buffer += text.decode(raw)
        while True:
            buffer = buffer.lstrip()
            if not buffer:
                break
            try:
                chunk, end = decoder.raw_decode(buffer)
            except json.JSONDecodeError:
                break  # incomplete object, wait for more data
            buffer = buffer[end:]
            yield chunk


response = requests.post(
    "https://ayeto.ai/api/v3/chat",
    headers={"uni-api-key": os.environ["AYETO_API_KEY"]},
    json={
        "conversation_id": str(uuid.uuid4()),
        "model": "gpt-5-mini",
        "message": "Explain recursion with a short example.",
        "stream": True,
        "runner_version": "2",
    },
    stream=True,
    timeout=(10, 600),
)
if response.status_code != 200:  # failed before the stream started
    raise RuntimeError(f"{response.status_code}: {response.json()['detail']}")

for chunk in iter_chunks(response):
    if chunk["error"]:
        error = chunk["error"]
        raise RuntimeError(f"{error['status']}: {error['detail']}")
    event = chunk["data"]
    if not isinstance(event, dict):
        continue  # keepalive
    if event["type"] == "text":
        print(event["delta"], end="", flush=True)
    elif event["type"] == "tool_call_start":
        print(f"\n[{event['tool_display_name']}]", flush=True)
    elif event["type"] == "status" and event["phase"] == "model_selected":
        print(f"[answering: {event['data']['model']}]", flush=True)
print()

Avec runner_version "1", chunk["data"] est une chaîne : affichez-la ou ajoutez-la telle quelle.

JavaScript

javascript
// Yields the JSON chunks of a streamed response.
async function* readChunks(response) {
  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let buffer = "";
  let pos = 0;
  let depth = 0;
  let inString = false;
  let escaped = false;
  while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });
    for (; pos < buffer.length; pos++) {
      const c = buffer[pos];
      if (inString) {
        if (escaped) escaped = false;
        else if (c === "\\") escaped = true;
        else if (c === '"') inString = false;
      } else if (c === '"') {
        inString = true;
      } else if (c === "{") {
        depth++;
      } else if (c === "}" && --depth === 0) {
        yield JSON.parse(buffer.slice(0, pos + 1));
        buffer = buffer.slice(pos + 1);
        pos = -1; // continue at the start of the remaining buffer
      }
    }
  }
}

const response = await fetch("https://ayeto.ai/api/v3/chat", {
  method: "POST",
  headers: {
    "uni-api-key": process.env.AYETO_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    conversation_id: crypto.randomUUID(),
    model: "gpt-5-mini",
    message: "Explain recursion with a short example.",
    stream: true,
    runner_version: "2",
  }),
});
if (!response.ok) {
  // failed before the stream started
  const body = await response.json();
  throw new Error(`${response.status}: ${JSON.stringify(body.detail)}`);
}

let answer = "";
for await (const chunk of readChunks(response)) {
  if (chunk.error) {
    throw new Error(`${chunk.error.status}: ${chunk.error.detail}`);
  }
  const event = chunk.data;
  if (typeof event !== "object" || event === null) continue; // keepalive
  if (event.type === "text") {
    answer += event.delta;
    process.stdout.write(event.delta);
  }
}

Les mêmes fonctions lisent le stream d'une exécution de workflow ; seuls les payloads data diffèrent, voir Exécution en streaming.