Diffusion en continu
Mettez stream à true et la réponse arrive sous forme de server-sent events pendant que le modèle l'écrit : d'abord le raisonnement, puis la réponse, puis un dernier chunk avec le motif de fin et l'utilisation. Utilisez-le chaque fois qu'une personne attend le texte, et pour les longues réponses.
POST https://api.shannon-ai.com/v1/chat/completions
Les SDK OpenAI vous donnent les chunks un par un. Lisez chaque chunk selon ce qu'il porte : un morceau de raisonnement, un morceau de la réponse, un appel d'outil, ou la fin de la réponse.
from openai import OpenAI, APIError
client = OpenAI(
api_key="YOUR_API_KEY",
base_url="https://api.shannon-ai.com/v1",
)
stream = client.chat.completions.create(
model="shannon-3",
messages=[{"role": "user", "content": "Explain server-sent events in three sentences."}],
stream=True,
)
answer, reasoning, tool_calls = [], [], []
finish_reason = usage = None
try:
for chunk in stream:
if chunk.usage:
usage = chunk.usage
if not chunk.choices:
continue
choice = chunk.choices[0]
delta = choice.delta
thought = getattr(delta, "reasoning_content", None)
if thought:
reasoning.append(thought)
if delta.content:
answer.append(delta.content)
print(delta.content, end="", flush=True)
for call in delta.tool_calls or []:
tool_calls.append((call.id, call.function.name, call.function.arguments))
if choice.finish_reason:
finish_reason = choice.finish_reason
except APIError as error:
# A failure after the stream started arrives as an error chunk.
print(f"\nstream failed: {error}")
print(f"\n{finish_reason} {usage}") import OpenAI from "openai";
const client = new OpenAI({
apiKey: "YOUR_API_KEY",
baseURL: "https://api.shannon-ai.com/v1",
});
const stream = await client.chat.completions.create({
model: "shannon-3",
messages: [{ role: "user", content: "Explain server-sent events in three sentences." }],
stream: true,
});
let answer = "";
let reasoning = "";
const toolCalls = [];
let finishReason = null;
let usage = null;
try {
for await (const chunk of stream) {
if (chunk.usage) usage = chunk.usage;
const choice = chunk.choices?.[0];
if (!choice) continue;
const delta = choice.delta;
if (delta.reasoning_content) reasoning += delta.reasoning_content;
if (delta.content) {
answer += delta.content;
process.stdout.write(delta.content);
}
for (const call of delta.tool_calls ?? []) {
toolCalls.push({ id: call.id, name: call.function.name, arguments: call.function.arguments });
}
if (choice.finish_reason) finishReason = choice.finish_reason;
}
} catch (error) {
// A failure after the stream started arrives as an error chunk.
console.error("\nstream failed:", error.message);
}
console.log("\n", finishReason, usage); curl -N https://api.shannon-ai.com/v1/chat/completions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "shannon-3",
"messages": [{"role": "user", "content": "Explain server-sent events in three sentences."}],
"stream": true
}' Comment un flux est structuré
- La réponse a le statut
200et les en-têtescontent-type: text/event-streametcache-control: no-cache. - Chaque événement est une ligne qui commence par
data:et qui est suivie d'une ligne vide. Le texte aprèsdata:est un chunk JSON. Il n'y a pas de lignesevent:niid:. - Une ligne qui commence par deux-points, comme
: ping, est un commentaire qui garde la connexion ouverte. Ignorez-la. - Le flux se termine par
data: [DONE]. Un flux qui s'arrête sans cette ligne est incomplet.
Les chunks dans l'ordre
Chaque chunk est un objet chat.completion.chunk :
{
"id": "chatcmpl-5f0c1e7a9b3d4c62a8e1f07d2b46c9a3",
"object": "chat.completion.chunk",
"created": 1791625200,
"model": "shannon-3",
"choices": [
{
"index": 0,
"delta": {
"content": "Server-sent"
},
"finish_reason": null
}
]
} Tous les chunks d'un même flux portent les mêmes id, created et model. choices contient toujours une seule entrée. Son delta contient ce qui est nouveau, et finish_reason vaut null jusqu'au dernier chunk.
| Clé dans delta | Type | Description | Envoyé par |
|---|---|---|---|
role | string | Toujours assistant. Il arrive une seule fois, au début du flux. | Modèles open-weight hébergés, shannon-2-lite, shannon-2-pro |
reasoning_content | string | Un morceau du raisonnement du modèle. Assemblez les morceaux dans l'ordre. | Modèles open-weight hébergés, famille Shannon 3, shannon-1.6-pro, shannon-coder-1 |
content | string | Un morceau de la réponse. Assemblez les morceaux dans l'ordre. | Tous les modèles |
tool_calls | array | Un appel d'outil complet. Voir Deltas d'appel d'outil. | Tous les modèles |
Le raisonnement vient en premier, puis la réponse, puis les éventuels appels d'outil. Lisez un chunk par les clés de son delta, pas par sa place dans le flux : un delta peut contenir role avec le premier morceau de texte, et le dernier est vide.
Un flux entier tel qu'il circule. Pour garder des lignes courtes, id, object, created et model sont omis de chaque chunk ici.
data: {"choices":[{"index":0,"delta":{"reasoning_content":"Three sentences:"},"finish_reason":null}]}
data: {"choices":[{"index":0,"delta":{"reasoning_content":" what it is, how it is framed, why it is used."},"finish_reason":null}]}
: ping
data: {"choices":[{"index":0,"delta":{"content":"Server-sent"},"finish_reason":null}]}
data: {"choices":[{"index":0,"delta":{"content":" events let a server push text to a client over one HTTP reply."},"finish_reason":null}]}
data: {"choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":1190,"completion_tokens":84,"total_tokens":1274}}
data: [DONE] Deltas de raisonnement
Un modèle qui raisonne envoie son raisonnement dans delta.reasoning_content avant la réponse. Gardez les deux textes séparés : affichez le raisonnement comme une partie distincte et repliable, ou omettez-le.
Les modèles open-weight hébergés n'envoient aucun raisonnement quand reasoning_effort vaut off, ni quand la requête a un response_format.
shannon-2-lite et shannon-2-pro raisonnent aussi avant de répondre. Sur ce point de terminaison, ils envoient des lignes de commentaire : thinking pendant ce temps, puis la réponse.
reasoning_content ne figure pas dans les modèles typés des SDK OpenAI. En Python, lisez-le avec getattr(delta, "reasoning_content", None) ; en JavaScript, delta.reasoning_content fonctionne tel quel.
Deltas d'appel d'outil
Un appel d'outil arrive entier : un chunk par appel, dont le delta contient l'appel avec la chaîne arguments complète. index numérote les appels de la réponse à partir de 0. Le code qui assemble arguments morceau par morceau fonctionne sans changement, car il n'assemble qu'un seul morceau.
{
"tool_calls": [
{
"index": 0,
"id": "call_3d9a7c1e5b2f4a60c8e1d7f09b24a6c5",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\":\"Paris\"}"
}
}
]
} Après les appels, le dernier chunk a finish_reason valant tool_calls.
Quand la requête a tools, shannon-1.6-lite, shannon-1.6-pro et shannon-coder-1 envoient le texte de la réponse en un seul delta à la fin de la réponse.
Le dernier chunk
Le dernier chunk avant data: [DONE] a un delta vide, le finish_reason et l'usage de la requête, tous dans le même chunk. Chaque flux l'a ; vous n'avez pas besoin de stream_options.
{
"id": "chatcmpl-5f0c1e7a9b3d4c62a8e1f07d2b46c9a3",
"object": "chat.completion.chunk",
"created": 1791625200,
"model": "shannon-3",
"choices": [
{
"index": 0,
"delta": {},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 1190,
"completion_tokens": 84,
"total_tokens": 1274
}
} Sur les modèles open-weight hébergés, usage contient aussi prompt_tokens_details.cached_tokens et completion_tokens_details.reasoning_tokens.
Après une requête avec web_search: true dont la recherche a trouvé quelque chose, le dernier chunk porte aussi sources, et un chunk avant lui porte les repères de citation sous la forme delta.annotations. Recherche Web intégrée
| finish_reason | Description | Envoyé par |
|---|---|---|
stop | Le modèle a terminé sa réponse, ou une chaîne stop est apparue. | Tous les modèles |
tool_calls | La réponse contient un ou plusieurs appels d'outil. | Tous les modèles |
length | La réponse a été coupée à la limite de sortie. | Famille Shannon 3, shannon-1.6-lite, shannon-1.6-pro, shannon-coder-1 |
Les champs de usage sont décrits avec le point de terminaison. Chat Completions
Lignes keep-alive
Tant que le modèle travaille et n'a encore rien à envoyer, le flux porte des lignes de commentaire. Elles ne contiennent aucune donnée. Un parseur SSE les ignore tout seul ; le code qui lit des lignes brutes doit ignorer toute ligne qui commence par deux-points.
| Ligne | Envoyé par | Quand |
|---|---|---|
: ping | Famille Shannon 3, shannon-1.6-lite, shannon-1.6-pro, shannon-coder-1 | Toutes les 15 secondes. |
: keepalive | Modèles open-weight hébergés | Toutes les 15 secondes pendant que le modèle travaille. |
: thinking | shannon-2-lite, shannon-2-pro | Pendant que le modèle raisonne. |
Erreurs à l'intérieur d'un flux
Une fois qu'un flux a commencé, son statut est 200, donc un échec arrive à l'intérieur du flux : un chunk avec un membre error là où se trouverait choices.
data: {"error":{"message":"The model backend failed to answer. Please retry.","type":"api_error","code":null,"param":null}}
data: {"choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":1190,"completion_tokens":12,"total_tokens":1202}}
data: [DONE] Après le chunk d'erreur, le dernier chunk, avec finish_reason et usage, et data: [DONE] suivent quand même. Vérifiez la présence de error dans chaque chunk. Sans cette vérification, une réponse qui a échoué à mi-chemin paraît complète.
| Type | Code | Message | Quand |
|---|---|---|---|
api_error | The model backend failed to answer. Please retry. | Le modèle a échoué, ou n'a rien écrit. Renvoyez la requête. | |
rate_limit_error | Shannon routes are temporarily busy. Please retry. | Le modèle est occupé en ce moment. Attendez quelques secondes et renvoyez la requête. | |
invalid_request_error | context_length_exceeded | La conversation est plus longue que la fenêtre de contexte du modèle. Raccourcissez-la avant de renvoyer la requête. Le texte du message varie. |
Les modèles open-weight hébergés envoient le membre error avec seulement type et message.
Les SDK OpenAI Python et JavaScript lèvent un chunk d'erreur comme une erreur d'API pendant l'itération, donc entourez la boucle de votre gestion d'erreurs habituelle.
Les codes de statut, le corps d'erreur et ce qu'il faut réessayer ont leur propre page. Gestion des erreurs
Fermer la connexion
Fermer la connexion arrête la livraison, pas la requête. Le modèle écrit la réponse jusqu'au bout et la requête est facturée comme si vous l'aviez lue en entier. Pour payer moins, demandez moins : fixez un max_tokens plus bas sur un modèle qui l'applique.
Un flux peut aussi s'arrêter plus tôt en route vers vous, par exemple lors d'une coupure réseau ou pendant la mise à jour d'un serveur. Il s'arrête alors sans le dernier chunk et sans data: [DONE]. Considérez cette réponse comme incomplète et renvoyez la requête.
Lire un flux sans SDK
Un lecteur correct fait cinq choses : il envoie les erreurs qui arrivent avant le flux vers sa gestion d'erreurs, découpe le corps en lignes, ignore les lignes de commentaire, s'arrête à data: [DONE] et vérifie la présence de error dans chaque chunk.
import json
import requests
response = requests.post(
"https://api.shannon-ai.com/v1/chat/completions",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"model": "shannon-3",
"messages": [{"role": "user", "content": "Explain server-sent events in three sentences."}],
"stream": True,
},
stream=True,
)
# 1. Errors before the stream are plain JSON with their own status.
if response.status_code != 200:
raise RuntimeError(f"{response.status_code}: {response.text}")
answer, done = [], False
for raw in response.iter_lines(): # 2. one line at a time
line = raw.decode("utf-8")
if not line.startswith("data: "): # 3. empty lines and ": ping" comments
continue
data = line[len("data: "):]
if data == "[DONE]": # 4. the end of the stream
done = True
break
chunk = json.loads(data)
if "error" in chunk: # 5. a failure inside the stream
raise RuntimeError(chunk["error"]["message"])
delta = chunk["choices"][0]["delta"]
if delta.get("content"):
answer.append(delta["content"])
if chunk.get("usage"):
print(chunk["choices"][0]["finish_reason"], chunk["usage"])
if not done:
raise RuntimeError("the stream ended before [DONE]")
print("".join(answer)) const response = await fetch("https://api.shannon-ai.com/v1/chat/completions", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "shannon-3",
messages: [{ role: "user", content: "Explain server-sent events in three sentences." }],
stream: true,
}),
});
// 1. Errors before the stream are plain JSON with their own status.
if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);
const decoder = new TextDecoder();
let buffer = "";
let answer = "";
let done = false;
read: for await (const bytes of response.body) {
buffer += decoder.decode(bytes, { stream: true });
const lines = buffer.split("\n"); // 2. one line at a time
buffer = lines.pop(); // keep the unfinished line
for (const line of lines) {
if (!line.startsWith("data: ")) continue; // 3. empty lines and ": ping" comments
const data = line.slice(6);
if (data === "[DONE]") { // 4. the end of the stream
done = true;
break read;
}
const chunk = JSON.parse(data);
if (chunk.error) throw new Error(chunk.error.message); // 5. a failure inside the stream
const choice = chunk.choices[0];
if (choice.delta.content) answer += choice.delta.content;
if (chunk.usage) console.log(choice.finish_reason, chunk.usage);
}
}
if (!done) throw new Error("the stream ended before [DONE]");
console.log(answer); L'exemple Python utilise le paquet requests. L'exemple JavaScript s'exécute sur Node.js 18 ou une version plus récente.
Flux des autres formats
Les points de terminaison Responses et Messages envoient aussi en streaming. Leurs trames portent une ligne event: avec le nom de l'événement et une ligne data: avec son JSON. Chaque page liste ses événements dans l'ordre.
| Dans le flux | Chat Completions | Responses | Messages |
|---|---|---|---|
| Trames | Lignes data: | Lignes event: et data: | Lignes event: et data: |
| Le raisonnement arrive dans | delta.reasoning_content | response.reasoning_summary_text.delta | thinking_delta |
| Le texte de la réponse arrive dans | delta.content | response.output_text.delta | text_delta |
| L'utilisation arrive dans | Le dernier chunk | response.completed | message_delta |
| Le flux se termine par | Le dernier chunk, puis data: [DONE] | response.completed | message_stop |