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}") 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 um stream é estruturado
- A resposta tem status
200e os cabeçalhoscontent-type: text/event-streamecache-control: no-cache. - Cada evento é uma linha que começa com
data:e é seguida de uma linha vazia. O texto depois dedata:é um chunk JSON. Não há linhasevent:nemid:. - 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:
{
"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.
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.
{
"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.
{
"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.
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)) 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 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 Completions | Responses | Messages |
|---|---|---|---|
| Frames | Linhas data: | Linhas event: e data: | Linhas event: e data: |
| O raciocínio chega em | delta.reasoning_content | response.reasoning_summary_text.delta | thinking_delta |
| O texto da resposta chega em | delta.content | response.output_text.delta | text_delta |
| O uso chega em | O último chunk | response.completed | message_delta |
| O stream termina com | O último chunk, depois data: [DONE] | response.completed | message_stop |