Потоковая передача
Задайте stream равным true, и ответ придет как server-sent events по мере того, как его пишет модель: сначала reasoning, затем ответ, затем последний чанк с причиной завершения и данными об использовании. Используйте это, когда текст ждет человек, и для длинных ответов.
POST https://api.shannon-ai.com/v1/chat/completions
OpenAI SDK отдают чанки по одному. Читайте каждый чанк по тому, что он несет: фрагмент reasoning, фрагмент ответа, вызов инструмента или конец ответа.
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
}' Как устроены фреймы потока
- Ответ имеет статус
200и заголовкиcontent-type: text/event-streamиcache-control: no-cache. - Каждое событие — одна строка, которая начинается с
data:и за которой следует пустая строка. Текст послеdata:— один JSON-чанк. Строкevent:иid:нет. - Строка, которая начинается с двоеточия, например
: ping, — это комментарий, поддерживающий соединение открытым. Пропускайте ее. - Поток заканчивается строкой
data: [DONE]. Поток, оборвавшийся без нее, неполный.
Чанки по порядку
Каждый чанк — это объект 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
}
]
} У всех чанков одного потока одинаковые id, created и model. В choices всегда одна запись. Ее delta содержит новое, а finish_reason равен null до последнего чанка.
| Ключ в delta | Тип | Описание | Отправляется |
|---|---|---|---|
role | string | Всегда assistant. Приходит один раз, в начале потока. | Размещенные модели с открытыми весами, shannon-2-lite, shannon-2-pro |
reasoning_content | string | Фрагмент рассуждений модели. Склеивайте фрагменты по порядку. | Размещенные модели с открытыми весами, семейство Shannon 3, shannon-1.6-pro, shannon-coder-1 |
content | string | Фрагмент ответа. Склеивайте фрагменты по порядку. | Все модели |
tool_calls | array | Один полный вызов инструмента. См. «Дельты вызова инструмента». | Все модели |
Сначала идет reasoning, затем ответ, затем вызовы инструментов, если они есть. Читайте чанк по ключам его delta, а не по месту в потоке: delta может содержать role вместе с первым фрагментом текста, а последняя — пуста.
Весь поток в том виде, в каком он передается. Чтобы строки были короче, id, object, created и model в каждом чанке здесь опущены.
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] Дельты reasoning
Модель, которая рассуждает, отправляет рассуждения в delta.reasoning_content перед ответом. Держите два текста раздельно: показывайте reasoning как отдельную сворачиваемую часть или не показывайте вовсе.
Размещенные модели с открытыми весами не отправляют reasoning, когда reasoning_effort равен off, а также когда в запросе есть response_format.
shannon-2-lite и shannon-2-pro тоже рассуждают перед ответом. На этом эндпоинте, пока они рассуждают, они отправляют строки-комментарии : thinking, а затем ответ.
reasoning_content нет в типизированных моделях OpenAI SDK. В Python читайте его через getattr(delta, "reasoning_content", None); в JavaScript delta.reasoning_content работает как есть.
Дельты вызова инструмента
Вызов инструмента приходит целиком: один чанк на вызов, delta которого содержит вызов с полной строкой arguments. index нумерует вызовы ответа с 0. Код, который склеивает arguments по частям, работает без изменений, потому что склеивает одну часть.
{
"tool_calls": [
{
"index": 0,
"id": "call_3d9a7c1e5b2f4a60c8e1d7f09b24a6c5",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\":\"Paris\"}"
}
}
]
} После вызовов в последнем чанке finish_reason равен tool_calls.
Если в запросе есть tools, shannon-1.6-lite, shannon-1.6-pro и shannon-coder-1 отправляют текст ответа одной дельтой в конце ответа.
Последний чанк
Последний чанк перед data: [DONE] содержит пустую delta, finish_reason и usage запроса, все в одном чанке. Он есть в каждом потоке; 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
}
} У размещенных моделей с открытыми весами в usage также есть prompt_tokens_details.cached_tokens и completion_tokens_details.reasoning_tokens.
После запроса с web_search: true, поиск которого что-то нашел, последний чанк несет также sources, а чанк перед ним несет метки цитирования как delta.annotations. Встроенный веб‑поиск
| finish_reason | Описание | Отправляется |
|---|---|---|
stop | Модель закончила ответ, или появилась строка stop. | Все модели |
tool_calls | Ответ содержит один или несколько вызовов инструментов. | Все модели |
length | Ответ оборван на лимите вывода. | Семейство Shannon 3, shannon-1.6-lite, shannon-1.6-pro, shannon-coder-1 |
Поля usage описаны вместе с эндпоинтом. Chat Completions
Строки keep-alive
Пока модель работает и ей еще нечего отправить, поток передает строки-комментарии. Данных в них нет. SSE-парсер пропускает их сам; код, читающий сырые строки, должен пропускать каждую строку, которая начинается с двоеточия.
| Строка | Отправляется | Когда |
|---|---|---|
: ping | Семейство Shannon 3, shannon-1.6-lite, shannon-1.6-pro, shannon-coder-1 | Каждые 15 секунд. |
: keepalive | Размещенные модели с открытыми весами | Каждые 15 секунд, пока модель работает. |
: thinking | shannon-2-lite, shannon-2-pro | Пока модель рассуждает. |
Ошибки внутри потока
После начала потока его статус равен 200, поэтому сбой приходит внутри потока: чанком с членом error на месте 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] После чанка с ошибкой по-прежнему следуют последний чанк с finish_reason и usage и data: [DONE]. Проверяйте каждый чанк на наличие error. Без этой проверки ответ, оборвавшийся на середине, выглядит завершенным.
| Тип | Код | Сообщение | Когда |
|---|---|---|---|
api_error | The model backend failed to answer. Please retry. | Модель дала сбой или ничего не написала. Отправьте запрос снова. | |
rate_limit_error | Shannon routes are temporarily busy. Please retry. | В данный момент модель занята. Подождите несколько секунд и отправьте запрос снова. | |
invalid_request_error | context_length_exceeded | Разговор длиннее контекстного окна модели. Сократите его, прежде чем отправлять снова. Текст сообщения может различаться. |
Размещенные модели с открытыми весами отправляют член error только с type и message.
OpenAI SDK для Python и JavaScript выбрасывают чанк с ошибкой как ошибку API во время итерации, поэтому оберните цикл в свою обычную обработку ошибок.
Коды статуса, тело ошибки и то, что стоит повторять, описаны на отдельной странице. Обработка ошибок
Закрытие соединения
Закрытие соединения останавливает доставку, но не запрос. Модель допишет ответ до конца, и запрос будет оплачен так, как будто вы прочитали его целиком. Чтобы платить меньше, просите меньше: задайте меньший max_tokens для модели, которая его применяет.
Поток может также оборваться раньше по дороге к вам, например при обрыве сети или во время обновления сервера. Тогда он останавливается без последнего чанка и без data: [DONE]. Считайте такой ответ неполным и отправьте запрос снова.
Чтение потока без SDK
Правильный читатель потока делает пять вещей: передает ошибки, пришедшие до потока, в свою обработку ошибок, делит тело на строки, пропускает строки-комментарии, останавливается на data: [DONE] и проверяет каждый чанк на 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); В примере на Python используется пакет requests. Пример на JavaScript работает на Node.js 18 или новее.
Потоки в других форматах
Эндпоинты Responses и Messages тоже поддерживают стриминг. Их фреймы содержат строку event: с именем события и строку data: с его JSON. На странице каждого эндпоинта события перечислены по порядку.
| В потоке | Chat Completions | Responses | Messages |
|---|---|---|---|
| Фреймы | Строки data: | Строки event: и data: | Строки event: и data: |
| Reasoning приходит в | delta.reasoning_content | response.reasoning_summary_text.delta | thinking_delta |
| Текст ответа приходит в | delta.content | response.output_text.delta | text_delta |
| Данные об использовании приходят в | Последний чанк | response.completed | message_delta |
| Поток заканчивается | Последний чанк, затем data: [DONE] | response.completed | message_stop |