Стримінг
Задайте stream у true, і відповідь надходитиме як server-sent events, поки модель її пише: спершу міркування, потім відповідь, потім останній чанк із причиною завершення та usage. Використовуйте це щоразу, коли людина чекає на текст, і для довгих відповідей.
POST https://api.shannon-ai.com/v1/chat/completions
SDK OpenAI віддають вам чанки по одному. Читайте кожен чанк за тим, що він несе: шматок міркувань, шматок відповіді, виклик інструмента або кінець відповіді.
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. Надходить один раз, на початку стріму. | Хостовані open-weight моделі, shannon-2-lite, shannon-2-pro |
reasoning_content | string | Шматок міркувань моделі. З'єднуйте шматки по порядку. | Хостовані open-weight моделі, сімейство Shannon 3, shannon-1.6-pro, shannon-coder-1 |
content | string | Шматок відповіді. З'єднуйте шматки по порядку. | Усі моделі |
tool_calls | array | Один повний виклик інструмента. Див. Tool call deltas. | Усі моделі |
Спершу міркування, потім відповідь, потім виклики інструментів. Читайте чанк за ключами в його 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] Дельти міркувань
Модель, що міркує, надсилає міркування в delta.reasoning_content перед відповіддю. Тримайте ці два тексти окремо: показуйте міркування як окрему частину, що згортається, або не показуйте зовсім.
Хостовані open-weight моделі не надсилають міркувань, коли reasoning_effort дорівнює off, і коли запит має response_format.
shannon-2-lite і shannon-2-pro теж міркують перед відповіддю. На цьому ендпоінті вони, поки міркують, надсилають рядки-коментарі : thinking, а потім відповідь.
reasoning_content немає в типізованих моделях SDK OpenAI. У 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
}
} На хостованих open-weight моделях usage також містить prompt_tokens_details.cached_tokens і completion_tokens_details.reasoning_tokens.
Після запиту з web_search: true, пошук якого щось знайшов, останній chunk також містить sources, а один chunk перед ним містить позначки цитування як 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 | Хостовані open-weight моделі | Кожні 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 | Розмова довша за контекстне вікно моделі. Скоротіть її, перш ніж надсилати знову. Текст повідомлення різниться. |
Хостовані open-weight моделі надсилають член error лише з type і message.
SDK OpenAI для 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: |
| Міркування надходять у | delta.reasoning_content | response.reasoning_summary_text.delta | thinking_delta |
| Текст відповіді надходить у | delta.content | response.output_text.delta | text_delta |
| Usage надходить у | Останній чанк | response.completed | message_delta |
| Стрім завершується | Останній чанк, потім data: [DONE] | response.completed | message_stop |