Vai al contenuto
Streaming

Streaming

Imposta stream su true e la risposta arriva come server-sent events mentre il modello la scrive: prima il ragionamento, poi la risposta, poi un ultimo chunk con il motivo di fine e l'utilizzo. Usalo ogni volta che una persona attende il testo, e per le risposte lunghe.

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

Gli SDK OpenAI ti danno i chunk uno per uno. Leggi ogni chunk per ciò che porta: un pezzo di ragionamento, un pezzo della risposta, una chiamata di tool o la fine della risposta.

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

Come è strutturato uno stream

  • La risposta ha stato 200 e gli header content-type: text/event-stream e cache-control: no-cache.
  • Ogni evento è una riga che inizia con data: ed è seguita da una riga vuota. Il testo dopo data: è un chunk JSON. Non ci sono righe event: o id:.
  • Una riga che inizia con i due punti, come : ping, è un commento che mantiene aperta la connessione. Saltala.
  • Lo stream termina con data: [DONE]. Uno stream che si ferma senza di esso è incompleto.

I chunk in ordine

Ogni chunk è un oggetto 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
    }
  ]
}

Tutti i chunk di uno stream portano gli stessi id, created e model. choices contiene sempre una sola voce. Il suo delta contiene ciò che è nuovo, e finish_reason è null fino all'ultimo chunk.

Chiave in delta Tipo Descrizione Inviato da
role string Sempre assistant. Arriva una volta sola, all'inizio dello stream. Modelli open-weight ospitati, shannon-2-lite, shannon-2-pro
reasoning_content string Un pezzo del ragionamento del modello. Unisci i pezzi in ordine. Modelli open-weight ospitati, famiglia Shannon 3, shannon-1.6-pro, shannon-coder-1
content string Un pezzo della risposta. Unisci i pezzi in ordine. Tutti i modelli
tool_calls array Una chiamata di tool completa. Vedi Delta delle chiamate di tool. Tutti i modelli

Prima viene il ragionamento, poi la risposta, poi le eventuali chiamate di tool. Leggi un chunk dalle chiavi nel suo delta, non dalla sua posizione nello stream: un delta può contenere role insieme al primo pezzo di testo, e l'ultimo è vuoto.

Un intero stream così come viaggia. Per mantenere brevi le righe, id, object, created e model sono omessi da ogni chunk qui.

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]

Delta di ragionamento

Un modello che ragiona invia il proprio ragionamento in delta.reasoning_content prima della risposta. Tieni separati i due testi: mostra il ragionamento come parte separata e comprimibile, oppure omettilo.

I modelli open-weight ospitati non inviano ragionamento quando reasoning_effort è off, e nessuno quando la richiesta ha un response_format.

Anche shannon-2-lite e shannon-2-pro ragionano prima di rispondere. Su questo endpoint, mentre lo fanno inviano righe di commento : thinking, e poi la risposta.

reasoning_content non è nei modelli tipizzati degli SDK OpenAI. In Python leggilo con getattr(delta, "reasoning_content", None); in JavaScript delta.reasoning_content funziona così com'è.

Delta delle chiamate di tool

Una chiamata di tool arriva intera: un chunk per chiamata, il cui delta contiene la chiamata con la stringa arguments completa. index numera le chiamate della risposta a partire da 0. Il codice che unisce arguments pezzo per pezzo funziona senza modifiche, perché unisce un solo pezzo.

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

Dopo le chiamate, l'ultimo chunk ha finish_reason tool_calls.

Quando la richiesta ha tools, shannon-1.6-lite, shannon-1.6-pro e shannon-coder-1 inviano il testo della risposta come un unico delta alla fine della risposta.

L'ultimo chunk

L'ultimo chunk prima di data: [DONE] ha un delta vuoto, il finish_reason e l'usage della richiesta, tutti nello stesso chunk. Ogni stream lo ha; non serve 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
  }
}

Sui modelli open-weight ospitati usage contiene anche prompt_tokens_details.cached_tokens e completion_tokens_details.reasoning_tokens.

Dopo una richiesta con web_search: true la cui ricerca ha trovato qualcosa, l'ultimo chunk contiene anche sources e un chunk prima di esso contiene i segni di citazione come delta.annotations. Ricerca web integrata

finish_reason Descrizione Inviato da
stop Il modello ha terminato la risposta, oppure è comparsa una stringa stop. Tutti i modelli
tool_calls La risposta contiene una o più chiamate di tool. Tutti i modelli
length La risposta è stata interrotta al limite di output. Famiglia Shannon 3, shannon-1.6-lite, shannon-1.6-pro, shannon-coder-1

I campi di usage sono descritti insieme all'endpoint. Chat Completions

Righe keep-alive

Mentre il modello lavora e non ha ancora nulla da inviare, lo stream porta righe di commento. Non contengono dati. Un parser SSE le salta da solo; il codice che legge righe grezze deve saltare ogni riga che inizia con i due punti.

Riga Inviato da Quando
: ping Famiglia Shannon 3, shannon-1.6-lite, shannon-1.6-pro, shannon-coder-1 Ogni 15 secondi.
: keepalive Modelli open-weight ospitati Ogni 15 secondi mentre il modello lavora.
: thinking shannon-2-lite, shannon-2-pro Mentre il modello ragiona.

Errori dentro uno stream

Una volta avviato uno stream il suo stato è 200, quindi un errore arriva dentro lo stream: un chunk con un membro error al posto di 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]

Dopo il chunk di errore seguono comunque l'ultimo chunk, con finish_reason e usage, e data: [DONE]. Controlla in ogni chunk la presenza di error. Senza questo controllo una risposta fallita a metà sembra completa.

Tipo Code Messaggio Quando
api_error The model backend failed to answer. Please retry. Il modello ha fallito, oppure non ha scritto nulla. Invia di nuovo la richiesta.
rate_limit_error Shannon routes are temporarily busy. Please retry. Il modello è occupato in questo momento. Attendi qualche secondo e invia di nuovo la richiesta.
invalid_request_error context_length_exceeded La conversazione è più lunga della finestra di contesto del modello. Accorciala prima di inviarla di nuovo. Il testo del messaggio varia.

I modelli open-weight ospitati inviano il membro error solo con type e message.

Gli SDK OpenAI Python e JavaScript sollevano un chunk di errore come errore API durante l'iterazione, quindi racchiudi il ciclo nella tua consueta gestione degli errori.

I codici di stato, il corpo dell'errore e cosa riprovare hanno una pagina propria. Gestione errori

Chiudere la connessione

Chiudere la connessione interrompe la consegna, non la richiesta. Il modello scrive la risposta fino alla fine e la richiesta viene addebitata come se l'avessi letta tutta. Per pagare meno, chiedi meno: imposta un max_tokens più basso su un modello che lo applica.

Uno stream può anche terminare prima del tempo lungo il percorso verso di te, per esempio per un'interruzione di rete o durante l'aggiornamento di un server. Si ferma allora senza l'ultimo chunk e senza data: [DONE]. Considera quella risposta incompleta e invia di nuovo la richiesta.

Leggere uno stream senza un SDK

Un lettore corretto fa cinque cose: invia alla propria gestione degli errori quelli che arrivano prima dello stream, divide il corpo in righe, salta le righe di commento, si ferma a data: [DONE] e controlla in ogni chunk la presenza di 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'esempio Python usa il pacchetto requests. L'esempio JavaScript gira su Node.js 18 o successivo.

Stream degli altri formati

Anche gli endpoint Responses e Messages usano lo streaming. I loro frame portano una riga event: con il nome dell'evento e una riga data: con il suo JSON. Ogni pagina elenca i propri eventi in ordine.

Nello stream Chat CompletionsResponsesMessages
Frame Righe data:Righe event: e data:Righe event: e data:
Il ragionamento arriva in delta.reasoning_contentresponse.reasoning_summary_text.deltathinking_delta
Il testo della risposta arriva in delta.contentresponse.output_text.deltatext_delta
L'utilizzo arriva in L'ultimo chunkresponse.completedmessage_delta
Lo stream termina con L'ultimo chunk, poi data: [DONE]response.completedmessage_stop