Saltar al contenido
Streaming

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

Cómo se estructura un stream

  • La respuesta tiene el estado 200 y los headers content-type: text/event-stream y cache-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 de data: es un fragmento JSON. No hay líneas event: ni id:.
  • 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:

JSON
{
  "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.

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]

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.

delta
{
  "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.

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

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.

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]

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

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 CompletionsResponsesMessages
Tramas Líneas data:Líneas event: y data:Líneas event: y data:
El razonamiento llega en delta.reasoning_contentresponse.reasoning_summary_text.deltathinking_delta
El texto de la respuesta llega en delta.contentresponse.output_text.deltatext_delta
El uso llega en El último fragmentoresponse.completedmessage_delta
El stream termina con El último fragmento, luego data: [DONE]response.completedmessage_stop