Aller au contenu
Diffusion en continu

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}")

Comment un flux est structuré

  • La réponse a le statut 200 et les en-têtes content-type: text/event-stream et cache-control: no-cache.
  • Chaque événement est une ligne qui commence par data: et qui est suivie d'une ligne vide. Le texte après data: est un chunk JSON. Il n'y a pas de lignes event: ni id:.
  • 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 :

JSON
{
  "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.

200 text/event-stream
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.

delta
{
  "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.

JSON
{
  "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.

200 text/event-stream
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))

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 CompletionsResponsesMessages
Trames Lignes data:Lignes event: et data:Lignes event: et data:
Le raisonnement arrive dans delta.reasoning_contentresponse.reasoning_summary_text.deltathinking_delta
Le texte de la réponse arrive dans delta.contentresponse.output_text.deltatext_delta
L'utilisation arrive dans Le dernier chunkresponse.completedmessage_delta
Le flux se termine par Le dernier chunk, puis data: [DONE]response.completedmessage_stop