Стрийминг
Задайте 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 | Едно пълно извикване на инструмент. Вижте Делти на извикване на инструмент. | Всички модели |
Първо идват разсъжденията, после отговорът, после евентуалните извиквания на инструменти. Четете фрагмента по ключовете в неговата 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, чието търсене е намерило нещо, последният фрагмент носи и 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 | Хоствани 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 |