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}") 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
}' Come è strutturato uno stream
- La risposta ha stato
200e gli headercontent-type: text/event-streamecache-control: no-cache. - Ogni evento è una riga che inizia con
data:ed è seguita da una riga vuota. Il testo dopodata:è un chunk JSON. Non ci sono righeevent:oid:. - 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:
{
"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.
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.
{
"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.
{
"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.
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)) 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'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 Completions | Responses | Messages |
|---|---|---|---|
| Frame | Righe data: | Righe event: e data: | Righe event: e data: |
| Il ragionamento arriva in | delta.reasoning_content | response.reasoning_summary_text.delta | thinking_delta |
| Il testo della risposta arriva in | delta.content | response.output_text.delta | text_delta |
| L'utilizzo arriva in | L'ultimo chunk | response.completed | message_delta |
| Lo stream termina con | L'ultimo chunk, poi data: [DONE] | response.completed | message_stop |