Streaming
Establece stream en true y la respuesta llega como server-sent events mientras el modelo la escribe: primero el razonamiento, luego la respuesta, y al final un último fragmento con el motivo de finalización y el uso. Úsalo siempre que una persona espere el texto, y para respuestas largas.
POST https://api.shannon-ai.com/v1/chat/completions
Los SDK de OpenAI te entregan los fragmentos uno a uno. Lee cada fragmento según lo que lleva: un fragmento de razonamiento, un fragmento de la respuesta, una llamada a herramienta o el final de la respuesta.
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
}' Cómo se estructura un stream
- La respuesta tiene el estado
200y los headerscontent-type: text/event-streamycache-control: no-cache. - Cada evento es una línea que empieza con
data:y va seguida de una línea vacía. El texto después dedata:es un fragmento JSON. No hay líneasevent:niid:. - Una línea que empieza con dos puntos, como
: ping, es un comentario que mantiene abierta la conexión. Omítela. - El stream termina con
data: [DONE]. Un stream que se detiene sin él está incompleto.
Fragmentos en orden
Cada fragmento es un objeto 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 los fragmentos de un stream llevan el mismo id, created y model. choices siempre contiene una entrada. Su delta contiene lo nuevo, y finish_reason es null hasta el último fragmento.
| Clave en delta | Tipo | Descripción | Enviado por |
|---|---|---|---|
role | string | Siempre assistant. Llega una vez, al inicio del stream. | Modelos de pesos abiertos alojados, shannon-2-lite, shannon-2-pro |
reasoning_content | string | Un fragmento del razonamiento del modelo. Une los fragmentos en orden. | Modelos de pesos abiertos alojados, familia Shannon 3, shannon-1.6-pro, shannon-coder-1 |
content | string | Un fragmento de la respuesta. Une los fragmentos en orden. | Todos los modelos |
tool_calls | array | Una llamada a herramienta completa. Consulta Deltas de llamadas a herramientas. | Todos los modelos |
Primero va el razonamiento, luego la respuesta y después las llamadas a herramientas. Lee un fragmento por las claves de su delta, no por su lugar en el stream: un delta puede contener role junto con el primer fragmento de texto, y el último está vacío.
Un stream completo tal como viaja. Para mantener cortas las líneas, aquí se omiten id, object, created y model en cada fragmento.
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 razonamiento
Un modelo que razona envía su razonamiento en delta.reasoning_content antes de la respuesta. Mantén los dos textos separados: muestra el razonamiento como una parte aparte y plegable, o déjalo fuera.
Los modelos de pesos abiertos alojados no envían razonamiento cuando reasoning_effort es off, ni cuando la solicitud tiene un response_format.
shannon-2-lite y shannon-2-pro también razonan antes de responder. En este endpoint envían líneas de comentario : thinking mientras lo hacen, y luego la respuesta.
reasoning_content no está en los modelos tipados de los SDK de OpenAI. En Python léelo con getattr(delta, "reasoning_content", None); en JavaScript delta.reasoning_content funciona tal cual.
Deltas de llamadas a herramientas
Una llamada a herramienta llega completa: un fragmento por llamada, cuyo delta contiene la llamada con la cadena arguments completa. index numera las llamadas de la respuesta desde 0. El código que une arguments pieza por pieza funciona sin cambios, porque une una sola pieza.
{
"tool_calls": [
{
"index": 0,
"id": "call_3d9a7c1e5b2f4a60c8e1d7f09b24a6c5",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\":\"Paris\"}"
}
}
]
} Después de las llamadas, el último fragmento tiene finish_reason tool_calls.
Cuando la solicitud tiene tools, shannon-1.6-lite, shannon-1.6-pro y shannon-coder-1 envían el texto de la respuesta como un solo delta al final de la respuesta.
El último fragmento
El último fragmento antes de data: [DONE] tiene un delta vacío, el finish_reason y el usage de la solicitud, todo en el mismo fragmento. Todo stream lo tiene; no 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
}
} En los modelos de pesos abiertos alojados usage también tiene prompt_tokens_details.cached_tokens y completion_tokens_details.reasoning_tokens.
Después de una solicitud con web_search: true cuya búsqueda encontró algo, el último fragmento también lleva sources, y un fragmento anterior lleva las marcas de cita como delta.annotations. Búsqueda web integrada
| finish_reason | Descripción | Enviado por |
|---|---|---|
stop | El modelo terminó su respuesta, o apareció una cadena de stop. | Todos los modelos |
tool_calls | La respuesta contiene una o más llamadas a herramientas. | Todos los modelos |
length | La respuesta se cortó en el límite de salida. | Familia Shannon 3, shannon-1.6-lite, shannon-1.6-pro, shannon-coder-1 |
Los campos de usage se describen con el endpoint. Chat Completions
Líneas keep-alive
Mientras el modelo trabaja y aún no tiene nada que enviar, el stream lleva líneas de comentario. No contienen datos. Un analizador SSE las omite solo; el código que lee líneas sin procesar debe omitir toda línea que empiece con dos puntos.
| Línea | Enviado por | Cuándo |
|---|---|---|
: ping | Familia Shannon 3, shannon-1.6-lite, shannon-1.6-pro, shannon-coder-1 | Cada 15 segundos. |
: keepalive | Modelos de pesos abiertos alojados | Cada 15 segundos mientras el modelo trabaja. |
: thinking | shannon-2-lite, shannon-2-pro | Mientras el modelo razona. |
Errores dentro de un stream
Una vez que un stream ha empezado, su estado es 200, así que un fallo llega dentro del stream: un fragmento con un miembro error donde irían los 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] Después del fragmento de error siguen llegando el último fragmento, con finish_reason y usage, y data: [DONE]. Revisa en cada fragmento si hay error. Sin esa comprobación, una respuesta que falló a medias parece completa.
| Tipo | Código | Mensaje | Cuándo |
|---|---|---|---|
api_error | The model backend failed to answer. Please retry. | El modelo falló o no escribió nada. Envía la solicitud de nuevo. | |
rate_limit_error | Shannon routes are temporarily busy. Please retry. | El modelo está ocupado en este momento. Espera unos segundos y envía la solicitud de nuevo. | |
invalid_request_error | context_length_exceeded | La conversación es más larga que la ventana de contexto del modelo. Acórtala antes de enviarla de nuevo. El texto del mensaje varía. |
Los modelos de pesos abiertos alojados envían el miembro error solo con type y message.
Los SDK de OpenAI para Python y JavaScript lanzan un fragmento de error como error de API mientras iteras, así que envuelve el bucle en tu manejo de errores habitual.
Los códigos de estado, el cuerpo del error y qué reintentar tienen su propia página. Manejo de errores
Cerrar la conexión
Cerrar la conexión detiene la entrega, no la solicitud. El modelo escribe la respuesta hasta el final y la solicitud se cobra como si la hubieras leído entera. Para pagar menos, pide menos: establece un max_tokens más bajo en un modelo que lo aplique.
Un stream también puede terminar antes de tiempo en su camino hacia ti, por ejemplo por un corte de red o mientras se actualiza un servidor. Entonces se detiene sin el último fragmento y sin data: [DONE]. Trata esa respuesta como incompleta y envía la solicitud de nuevo.
Leer un stream sin un SDK
Un lector correcto hace cinco cosas: envía a su manejo de errores los errores que llegan antes del stream, divide el cuerpo en líneas, omite las líneas de comentario, se detiene en data: [DONE] y revisa en cada fragmento si hay 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); El ejemplo de Python usa el paquete requests. El ejemplo de JavaScript se ejecuta en Node.js 18 o posterior.
Streams de los otros formatos
Los endpoints Responses y Messages también hacen stream. Sus tramas llevan una línea event: con el nombre del evento y una línea data: con su JSON. Cada página enumera sus eventos en orden.
| En el stream | Chat Completions | Responses | Messages |
|---|---|---|---|
| Tramas | Líneas data: | Líneas event: y data: | Líneas event: y data: |
| El razonamiento llega en | delta.reasoning_content | response.reasoning_summary_text.delta | thinking_delta |
| El texto de la respuesta llega en | delta.content | response.output_text.delta | text_delta |
| El uso llega en | El último fragmento | response.completed | message_delta |
| El stream termina con | El último fragmento, luego data: [DONE] | response.completed | message_stop |