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 deContent-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:.
{"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.
{"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
errordé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.
{"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 :
{"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) :
{"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
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
// 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.