Streaming
Define stream como true e a resposta chega como server-sent events mentres o modelo a escribe: primeiro o razoamento, despois a resposta e por último un chunk co motivo de finalización e o uso. Úsao sempre que unha persoa agarde polo texto, e para respostas longas.
POST https://api.shannon-ai.com/v1/chat/completions
Os SDK de OpenAI dánche os chunks un por un. Le cada chunk segundo o que leva: un anaco de razoamento, un anaco da resposta, unha chamada a unha ferramenta ou o final da 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
}' Como se enmarca un stream
- A resposta ten o estado
200e as cabeceirascontent-type: text/event-streamecache-control: no-cache. - Cada evento é unha liña que comeza por
data:e vai seguida dunha liña baleira. O texto despois dedata:é un chunk JSON. Non hai liñasevent:ninid:. - Unha liña que comeza por dous puntos, como
: ping, é un comentario que mantén a conexión aberta. Sáltaa. - O stream remata con
data: [DONE]. Un stream que se detén sen el está incompleto.
Chunks por orde
Cada chunk é un obxecto 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
}
]
} Todos os chunks dun stream levan o mesmo id, created e model. choices contén sempre unha entrada. O seu delta contén o que é novo, e finish_reason é null ata o último chunk.
| Clave en delta | Tipo | Descrición | Enviado por |
|---|---|---|---|
role | string | Sempre assistant. Chega unha vez, ao comezo do stream. | Modelos open-weight alojados, shannon-2-lite, shannon-2-pro |
reasoning_content | string | Un anaco do razoamento do modelo. Une os anacos por orde. | Modelos open-weight alojados, familia Shannon 3, shannon-1.6-pro, shannon-coder-1 |
content | string | Un anaco da resposta. Une os anacos por orde. | Todos os modelos |
tool_calls | array | Unha chamada completa a unha ferramenta. Consulta Deltas de chamadas a ferramentas. | Todos os modelos |
O razoamento vén primeiro, despois a resposta e despois as chamadas a ferramentas. Le un chunk polas claves do seu delta, non polo seu lugar no stream: un delta pode conter role xunto co primeiro anaco de texto, e o último está baleiro.
Un stream completo tal como viaxa. Para manter as liñas curtas, aquí omítense id, object, created e model de cada chunk.
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 razoamento
Un modelo que razoa envía o seu razoamento en delta.reasoning_content antes da resposta. Mantén os dous textos separados: mostra o razoamento como unha parte aparte e pregable, ou omítoo.
Os modelos open-weight alojados non envían razoamento cando reasoning_effort é off, nin cando a solicitude ten un response_format.
shannon-2-lite e shannon-2-pro tamén razoan antes de responder. Neste endpoint envían liñas de comentario : thinking mentres o fan e despois a resposta.
reasoning_content non está nos modelos tipados dos SDK de OpenAI. En Python léeo con getattr(delta, "reasoning_content", None); en JavaScript delta.reasoning_content funciona tal cal.
Deltas de chamadas a ferramentas
Unha chamada a unha ferramenta chega enteira: un chunk por chamada, cuxo delta contén a chamada coa cadea arguments completa. index numera as chamadas da resposta desde 0. O código que une arguments anaco a anaco funciona sen cambios, porque une un só anaco.
{
"tool_calls": [
{
"index": 0,
"id": "call_3d9a7c1e5b2f4a60c8e1d7f09b24a6c5",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\":\"Paris\"}"
}
}
]
} Despois das chamadas, o último chunk ten finish_reason tool_calls.
Cando a solicitude ten tools, shannon-1.6-lite, shannon-1.6-pro e shannon-coder-1 envían o texto da resposta como un só delta ao final da resposta.
O último chunk
O último chunk antes de data: [DONE] ten un delta baleiro, o finish_reason e o usage da solicitude, todo no mesmo chunk. Todos os streams o teñen; non necesitas 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
}
} Nos modelos open-weight alojados, usage ten ademais prompt_tokens_details.cached_tokens e completion_tokens_details.reasoning_tokens.
Despois dunha solicitude con web_search: true cuxa busca atopou algo, o último chunk tamén leva sources, e un chunk anterior leva as marcas de cita como delta.annotations. Busca web
| finish_reason | Descrición | Enviado por |
|---|---|---|
stop | O modelo rematou a súa resposta, ou apareceu unha cadea de stop. | Todos os modelos |
tool_calls | A resposta contén unha ou varias chamadas a ferramentas. | Todos os modelos |
length | A resposta cortouse no límite de saída. | Familia Shannon 3, shannon-1.6-lite, shannon-1.6-pro, shannon-coder-1 |
Os campos de usage descríbense co endpoint. Chat Completions
Liñas keep-alive
Mentres o modelo traballa e aínda non ten nada que enviar, o stream leva liñas de comentario. Non conteñen datos. Un analizador SSE sáltaas por si mesmo; o código que le liñas en bruto debe saltar todas as liñas que comezan por dous puntos.
| Liña | Enviado por | Cando |
|---|---|---|
: ping | Familia Shannon 3, shannon-1.6-lite, shannon-1.6-pro, shannon-coder-1 | Cada 15 segundos. |
: keepalive | Modelos open-weight alojados | Cada 15 segundos mentres o modelo traballa. |
: thinking | shannon-2-lite, shannon-2-pro | Mentres o modelo razoa. |
Erros dentro dun stream
Unha vez que comeza un stream, o seu estado é 200, así que un fallo chega dentro do stream: un chunk cun membro error onde irían as 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] Despois do chunk de erro seguen chegando o último chunk, con finish_reason e usage, e data: [DONE]. Comproba en cada chunk se hai error. Sen esa comprobación, unha resposta que fallou a medio camiño parece completa.
| Tipo | Código | Mensaxe | Cando |
|---|---|---|---|
api_error | The model backend failed to answer. Please retry. | O modelo fallou, ou non escribiu nada. Envía a solicitude de novo. | |
rate_limit_error | Shannon routes are temporarily busy. Please retry. | O modelo está ocupado neste momento. Agarda uns segundos e envía a solicitude de novo. | |
invalid_request_error | context_length_exceeded | A conversa é máis longa que a ventá de contexto do modelo. Acúrtaa antes de enviala de novo. O texto da mensaxe varía. |
Os modelos open-weight alojados envían o membro error só con type e message.
Os SDK de OpenAI para Python e JavaScript lanzan un chunk de erro como un erro da API mentres iteras, así que envolve o bucle no teu tratamento de erros habitual.
Os códigos de estado, o corpo do erro e que reintentar teñen a súa propia páxina. Xestión de erros
Pechar a conexión
Pechar a conexión detén a entrega, non a solicitude. O modelo escribe a resposta ata o final e a solicitude cóbrase como se a leses toda. Para pagar menos, pide menos: define un max_tokens menor nun modelo que o aplique.
Un stream tamén pode rematar antes de tempo no seu camiño ata ti, por exemplo por unha rotura de rede ou mentres se actualiza un servidor. Entón detense sen o último chunk e sen data: [DONE]. Considera esa resposta como incompleta e envía a solicitude de novo.
Ler un stream sen SDK
Un lector correcto fai cinco cousas: envía ao seu tratamento de erros os erros que chegan antes do stream, divide o corpo en liñas, salta as liñas de comentario, detense en data: [DONE] e comproba en cada chunk se hai 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); O exemplo de Python usa o paquete requests. O exemplo de JavaScript funciona en Node.js 18 ou posterior.
Streams dos outros formatos
Os endpoints Responses e Messages tamén fan streaming. Os seus frames levan unha liña event: co nome do evento e unha liña data: co seu JSON. Cada páxina lista os seus eventos por orde.
| No stream | Chat Completions | Responses | Messages |
|---|---|---|---|
| Frames | Liñas data: | Liñas event: e data: | Liñas event: e data: |
| O razoamento chega en | delta.reasoning_content | response.reasoning_summary_text.delta | thinking_delta |
| O texto da resposta chega en | delta.content | response.output_text.delta | text_delta |
| O uso chega en | O último chunk | response.completed | message_delta |
| O stream remata con | O último chunk e despois data: [DONE] | response.completed | message_stop |