Salta al contingut
Streaming

Streaming

Posa stream a true i la resposta arriba com a server-sent events mentre el model l'escriu: primer el raonament, després la resposta, i al final un últim fragment amb el motiu de finalització i l'ús. Fes-ho servir sempre que una persona esperi el text, i per a respostes llargues.

POST https://api.shannon-ai.com/v1/chat/completions

Els SDK d'OpenAI et donen els fragments un per un. Llegeix cada fragment pel que porta: un troç de raonament, un troç de la resposta, una crida d'eina o el final de la resposta.

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

Com s'enquadra un stream

  • La resposta té l'estat 200 i les capçaleres content-type: text/event-stream i cache-control: no-cache.
  • Cada esdeveniment és una línia que comença per data: i va seguida d'una línia buida. El text després de data: és un fragment JSON. No hi ha línies event: ni id:.
  • Una línia que comença amb dos punts, com : ping, és un comentari que manté la connexió oberta. Ignora-la.
  • El stream acaba amb data: [DONE]. Un stream que s'atura sense aquesta línia és incomplet.

Fragments per ordre

Cada fragment és un objecte 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
    }
  ]
}

Tots els fragments d'un stream porten els mateixos id, created i model. choices conté sempre una sola entrada. El seu delta conté el que és nou, i finish_reason és null fins a l'últim fragment.

Clau a delta Tipus Descripció L'envia
role string Sempre assistant. Arriba una sola vegada, a l'inici del stream. Models open-weight hostejats, shannon-2-lite, shannon-2-pro
reasoning_content string Un troç del raonament del model. Uneix els troços per ordre. Models open-weight hostejats, família Shannon 3, shannon-1.6-pro, shannon-coder-1
content string Un troç de la resposta. Uneix els troços per ordre. Tots els models
tool_calls array Una crida d'eina completa. Vegeu Deltas de crides d'eina. Tots els models

El raonament ve primer, després la resposta, després les crides d'eina. Llegeix un fragment per les claus del seu delta, no pel lloc que ocupa al stream: un delta pot contenir role juntament amb el primer troç de text, i l'últim és buit.

Un stream sencer tal com viatja. Per mantenir les línies curtes, aquí s'han omès id, object, created i model de cada fragment.

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 raonament

Un model que raona envia el seu raonament a delta.reasoning_content abans de la resposta. Mantén els dos textos separats: mostra el raonament com una part a part que es pugui plegar, o omet-lo.

Els models open-weight hostejats no envien raonament quan reasoning_effort és off, ni quan la sol·licitud té un response_format.

shannon-2-lite i shannon-2-pro també raonen abans de respondre. En aquest endpoint envien línies de comentari : thinking mentre ho fan, i després la resposta.

reasoning_content no és als models tipats dels SDK d'OpenAI. A Python llegeix-lo amb getattr(delta, "reasoning_content", None); a JavaScript delta.reasoning_content funciona tal com és.

Deltas de crides d'eina

Una crida d'eina arriba sencera: un fragment per crida, el delta del qual conté la crida amb la cadena arguments completa. index numera les crides de la resposta des de 0. El codi que uneix arguments troç a troç funciona sense canvis, perquè uneix un sol troç.

delta
{
  "tool_calls": [
    {
      "index": 0,
      "id": "call_3d9a7c1e5b2f4a60c8e1d7f09b24a6c5",
      "type": "function",
      "function": {
        "name": "get_weather",
        "arguments": "{\"city\":\"Paris\"}"
      }
    }
  ]
}

Després de les crides, l'últim fragment té finish_reason tool_calls.

Quan la sol·licitud té tools, shannon-1.6-lite, shannon-1.6-pro i shannon-coder-1 envien el text de la resposta com un sol delta al final de la resposta.

L'últim fragment

L'últim fragment abans de data: [DONE] té un delta buit, el finish_reason i el usage de la sol·licitud, tot al mateix fragment. Tots els streams el tenen; no cal 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
  }
}

En els models open-weight hostejats, usage també té prompt_tokens_details.cached_tokens i completion_tokens_details.reasoning_tokens.

Després d'una sol·licitud amb web_search: true la cerca de la qual ha trobat alguna cosa, l'últim fragment també porta sources, i un fragment abans porta les marques de cita com a delta.annotations. Cerca web

finish_reason Descripció L'envia
stop El model ha acabat la seva resposta, o ha aparegut una cadena stop. Tots els models
tool_calls La resposta conté una o més crides d'eina. Tots els models
length La resposta s'ha tallat al límit de sortida. Família Shannon 3, shannon-1.6-lite, shannon-1.6-pro, shannon-coder-1

Els camps de usage es descriuen amb l'endpoint. Chat Completions

Línies keep-alive

Mentre el model treballa i encara no té res per enviar, el stream porta línies de comentari. No contenen dades. Un analitzador SSE les ignora per si mateix; el codi que llegeix línies en brut ha d'ignorar totes les línies que comencen amb dos punts.

Línia L'envia Quan
: ping Família Shannon 3, shannon-1.6-lite, shannon-1.6-pro, shannon-coder-1 Cada 15 segons.
: keepalive Models open-weight hostejats Cada 15 segons mentre el model treballa.
: thinking shannon-2-lite, shannon-2-pro Mentre el model raona.

Errors dins d'un stream

Un cop un stream ha començat, el seu estat és 200, així que un error arriba dins del stream: un fragment amb un membre error allà on hi hauria 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]

Després del fragment d'error encara segueixen l'últim fragment, amb finish_reason i usage, i data: [DONE]. Comprova a cada fragment si hi ha error. Sense aquesta comprovació, una resposta que ha fallat a mitges sembla completa.

Tipus Codi Missatge Quan
api_error The model backend failed to answer. Please retry. El model ha fallat, o no ha escrit res. Torna a enviar la sol·licitud.
rate_limit_error Shannon routes are temporarily busy. Please retry. El model està ocupat en aquest moment. Espera uns segons i torna a enviar la sol·licitud.
invalid_request_error context_length_exceeded La conversa és més llarga que la finestra de context del model. Escurça-la abans de tornar-la a enviar. El text del missatge varia.

Els models open-weight hostejats envien el membre error només amb type i message.

Els SDK d'OpenAI per a Python i JavaScript llancen un fragment d'error com a error d'API mentre iteres, així que envolta el bucle amb el teu tractament d'errors habitual.

Els codis d'estat, el cos de l'error i què cal reintentar tenen la seva pròpia pàgina. Gestió d’errors

Tancament de la connexió

Tancar la connexió atura el lliurament, no la sol·licitud. El model escriu la resposta fins al final i la sol·licitud es cobra com si l'haguessis llegida tota. Per pagar menys, demana menys: defineix un max_tokens més baix en un model que l'apliqui.

Un stream també pot acabar abans d'hora de camí cap a tu, per exemple per un tall de xarxa o mentre s'actualitza un servidor. Llavors s'atura sense l'últim fragment i sense data: [DONE]. Tracta aquesta resposta com a incompleta i torna a enviar la sol·licitud.

Lectura d'un stream sense SDK

Un lector correcte fa cinc coses: envia al seu tractament d'errors els errors que arriben abans del stream, divideix el cos en línies, ignora les línies de comentari, s'atura a data: [DONE] i comprova a cada fragment si hi ha error.

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 de Python fa servir el paquet requests. L'exemple de JavaScript s'executa a Node.js 18 o posterior.

Streams dels altres formats

Els endpoints Responses i Messages també fan streaming. Els seus frames porten una línia event: amb el nom de l'esdeveniment i una línia data: amb el seu JSON. Cada pàgina llista els seus esdeveniments per ordre.

Al stream Chat CompletionsResponsesMessages
Frames Línies data:Línies event: i data:Línies event: i data:
El raonament arriba a delta.reasoning_contentresponse.reasoning_summary_text.deltathinking_delta
El text de la resposta arriba a delta.contentresponse.output_text.deltatext_delta
L'ús arriba a L'últim fragmentresponse.completedmessage_delta
El stream acaba amb L'últim fragment, i després data: [DONE]response.completedmessage_stop