Ir para o conteúdo
Streaming

Streaming

Defina stream como true e a resposta chega como server-sent events enquanto o modelo a escreve: primeiro o raciocínio, depois a resposta, depois um último chunk com o motivo de término e o uso. Use-o sempre que uma pessoa estiver esperando o texto, e em respostas longas.

POST https://api.shannon-ai.com/v1/chat/completions

Os SDKs da OpenAI entregam os chunks um por um. Leia cada chunk pelo que ele traz: um trecho de raciocínio, um trecho da resposta, uma chamada de ferramenta ou o fim 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 um stream é estruturado

  • A resposta tem status 200 e os cabeçalhos content-type: text/event-stream e cache-control: no-cache.
  • Cada evento é uma linha que começa com data: e é seguida de uma linha vazia. O texto depois de data: é um chunk JSON. Não há linhas event: nem id:.
  • Uma linha que começa com dois-pontos, como : ping, é um comentário que mantém a conexão aberta. Ignore-a.
  • O stream termina com data: [DONE]. Um stream que para sem ele está incompleto.

Chunks em ordem

Cada chunk é um 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 os chunks de um mesmo stream trazem o mesmo id, created e model. choices sempre contém uma entrada. O seu delta contém o que é novo, e finish_reason é null até o último chunk.

Chave em delta Tipo Descrição Enviado por
role string Sempre assistant. Chega uma única vez, no início do stream. Modelos open-weight hospedados, shannon-2-lite, shannon-2-pro
reasoning_content string Um trecho do raciocínio do modelo. Junte os trechos em ordem. Modelos open-weight hospedados, família Shannon 3, shannon-1.6-pro, shannon-coder-1
content string Um trecho da resposta. Junte os trechos em ordem. Todos os modelos
tool_calls array Uma chamada de ferramenta completa. Veja Deltas de chamada de ferramenta. Todos os modelos

O raciocínio vem primeiro, depois a resposta, depois as eventuais chamadas de ferramenta. Leia um chunk pelas chaves do seu delta, não pela sua posição no stream: um delta pode conter role junto com o primeiro trecho de texto, e o último é vazio.

Um stream inteiro como ele trafega. Para manter as linhas curtas, id, object, created e model foram omitidos de cada chunk aqui.

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 raciocínio

Um modelo que raciocina envia o seu raciocínio em delta.reasoning_content antes da resposta. Mantenha os dois textos separados: mostre o raciocínio como uma parte separada e recolhível, ou deixe-o de fora.

Os modelos open-weight hospedados não enviam raciocínio quando reasoning_effort é off, nem quando a requisição tem um response_format.

shannon-2-lite e shannon-2-pro também raciocinam antes de responder. Neste endpoint, eles enviam linhas de comentário : thinking enquanto raciocinam, e depois a resposta.

reasoning_content não está nos modelos tipados dos SDKs da OpenAI. Em Python, leia-o com getattr(delta, "reasoning_content", None); em JavaScript, delta.reasoning_content funciona como está.

Deltas de chamada de ferramenta

Uma chamada de ferramenta chega inteira: um chunk por chamada, cujo delta contém a chamada com a string arguments completa. index numera as chamadas da resposta a partir de 0. O código que junta arguments pedaço por pedaço funciona sem alterações, porque junta um único pedaço.

delta
{
  "tool_calls": [
    {
      "index": 0,
      "id": "call_3d9a7c1e5b2f4a60c8e1d7f09b24a6c5",
      "type": "function",
      "function": {
        "name": "get_weather",
        "arguments": "{\"city\":\"Paris\"}"
      }
    }
  ]
}

Depois das chamadas, o último chunk tem finish_reason tool_calls.

Quando a requisição tem tools, shannon-1.6-lite, shannon-1.6-pro e shannon-coder-1 enviam o texto da resposta como um único delta no fim da resposta.

O último chunk

O último chunk antes de data: [DONE] tem um delta vazio, o finish_reason e o usage da requisição, tudo no mesmo chunk. Todo stream o tem; você não precisa de 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 hospedados, usage também tem prompt_tokens_details.cached_tokens e completion_tokens_details.reasoning_tokens.

Depois de uma requisição com web_search: true cuja busca encontrou algo, o último chunk também traz sources, e um chunk antes dele traz as marcas de citação como delta.annotations. Busca web integrada

finish_reason Descrição Enviado por
stop O modelo terminou a sua resposta, ou uma string de stop apareceu. Todos os modelos
tool_calls A resposta contém uma ou mais chamadas de ferramenta. Todos os modelos
length A resposta foi cortada no limite de saída. Família Shannon 3, shannon-1.6-lite, shannon-1.6-pro, shannon-coder-1

Os campos de usage são descritos junto com o endpoint. Chat Completions

Linhas keep-alive

Enquanto o modelo trabalha e ainda não tem nada a enviar, o stream traz linhas de comentário. Elas não contêm dados. Um parser SSE as ignora sozinho; o código que lê linhas brutas deve ignorar toda linha que começa com dois-pontos.

Linha Enviado por Quando
: ping Família Shannon 3, shannon-1.6-lite, shannon-1.6-pro, shannon-coder-1 A cada 15 segundos.
: keepalive Modelos open-weight hospedados A cada 15 segundos enquanto o modelo trabalha.
: thinking shannon-2-lite, shannon-2-pro Enquanto o modelo raciocina.

Erros dentro de um stream

Depois que um stream começa, o seu status é 200, então uma falha chega dentro do stream: um chunk com um membro error no lugar onde estaria 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]

Depois do chunk de erro, ainda vêm o último chunk, com finish_reason e usage, e data: [DONE]. Verifique se cada chunk tem error. Sem essa verificação, uma resposta que falhou no meio parece completa.

Tipo Código Mensagem Quando
api_error The model backend failed to answer. Please retry. O modelo falhou ou não escreveu nada. Envie a requisição novamente.
rate_limit_error Shannon routes are temporarily busy. Please retry. O modelo está ocupado neste momento. Aguarde alguns segundos e envie a requisição novamente.
invalid_request_error context_length_exceeded A conversa é maior do que a janela de contexto do modelo. Reduza-a antes de enviar novamente. O texto da mensagem varia.

Os modelos open-weight hospedados enviam o membro error apenas com type e message.

Os SDKs de Python e JavaScript da OpenAI lançam um chunk de erro como um erro de API enquanto você itera, então envolva o loop no seu tratamento de erros habitual.

Os códigos de status, o corpo do erro e o que tentar de novo têm a sua própria página. Tratamento de erros

Fechando a conexão

Fechar a conexão interrompe a entrega, não a requisição. O modelo escreve a resposta até o fim e a requisição é cobrada como se você tivesse lido tudo. Para pagar menos, peça menos: defina um max_tokens menor em um modelo que o aplique.

Um stream também pode terminar mais cedo no caminho até você, por exemplo em uma queda de rede ou durante a atualização de um servidor. Ele então para sem o último chunk e sem data: [DONE]. Trate essa resposta como incompleta e envie a requisição novamente.

Lendo um stream sem um SDK

Um leitor correto faz cinco coisas: envia os erros que chegam antes do stream para o seu tratamento de erros, divide o corpo em linhas, ignora as linhas de comentário, para em data: [DONE] e verifica se cada chunk tem 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 em Python usa o pacote requests. O exemplo em JavaScript roda no Node.js 18 ou posterior.

Streams dos outros formatos

Os endpoints Responses e Messages também usam stream. Os seus frames trazem uma linha event: com o nome do evento e uma linha data: com o seu JSON. Cada página lista os seus eventos em ordem.

No stream Chat CompletionsResponsesMessages
Frames Linhas data:Linhas event: e data:Linhas event: e data:
O raciocínio chega em delta.reasoning_contentresponse.reasoning_summary_text.deltathinking_delta
O texto da resposta chega em delta.contentresponse.output_text.deltatext_delta
O uso chega em O último chunkresponse.completedmessage_delta
O stream termina com O último chunk, depois data: [DONE]response.completedmessage_stop