Saltar ao contido
Streaming

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

Como se enmarca un stream

  • A resposta ten o estado 200 e as cabeceiras content-type: text/event-stream e cache-control: no-cache.
  • Cada evento é unha liña que comeza por data: e vai seguida dunha liña baleira. O texto despois de data: é un chunk JSON. Non hai liñas event: nin id:.
  • 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:

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

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

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

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

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.

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]

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

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 CompletionsResponsesMessages
Frames Liñas data:Liñas event: e data:Liñas event: e data:
O razoamento chega en delta.reasoning_contentresponse.reasoning_summary_text.deltathinking_delta
O texto da resposta chega en delta.contentresponse.output_text.deltatext_delta
O uso chega en O último chunkresponse.completedmessage_delta
O stream remata con O último chunk e despois data: [DONE]response.completedmessage_stop