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}") 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
}' Com s'enquadra un stream
- La resposta té l'estat
200i les capçalerescontent-type: text/event-streamicache-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 dedata:és un fragment JSON. No hi ha líniesevent:niid:. - 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:
{
"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.
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ç.
{
"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.
{
"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.
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)) 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 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 Completions | Responses | Messages |
|---|---|---|---|
| Frames | Línies data: | Línies event: i data: | Línies event: i data: |
| El raonament arriba a | delta.reasoning_content | response.reasoning_summary_text.delta | thinking_delta |
| El text de la resposta arriba a | delta.content | response.output_text.delta | text_delta |
| L'ús arriba a | L'últim fragment | response.completed | message_delta |
| El stream acaba amb | L'últim fragment, i després data: [DONE] | response.completed | message_stop |