استریم
stream را روی true بگذارید تا پاسخ همزمان با نوشتن مدل، بهصورت server-sent events برسد: ابتدا استدلال، سپس پاسخ، و در پایان یک chunk آخر با دلیل پایان و میزان مصرف. هر وقت کسی منتظر متن است و برای پاسخهای بلند از آن استفاده کنید.
POST https://api.shannon-ai.com/v1/chat/completions
SDKهای OpenAI chunkها را یکییکی به شما میدهند. هر chunk را بر اساس چیزی که حمل میکند بخوانید: بخشی از استدلال، بخشی از پاسخ، یک فراخوانی ابزار، یا پایان پاسخ.
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
}' قالببندی یک stream
- پاسخ وضعیت
200و هدرهایcontent-type: text/event-streamوcache-control: no-cacheدارد. - هر رویداد یک خط است که با
data:شروع میشود و یک خط خالی بعد از آن میآید. متن بعد ازdata:یک chunk از JSON است. خطی باevent:یاid:وجود ندارد. - خطی که با دو نقطه شروع شود، مانند
: ping، یک کامنت است که اتصال را باز نگه میدارد. از آن رد شوید. - stream با
data: [DONE]تمام میشود. streamی که بدون آن متوقف شود ناقص است.
chunkها به ترتیب
هر chunk یک شیء 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
}
]
} همه chunkهای یک stream id، created و model یکسان دارند. choices همیشه یک مورد دارد. delta آن آنچه را تازه است دارد و finish_reason تا آخرین chunk برابر null است.
| کلید در delta | نوع | توضیحات | ارسالشده توسط |
|---|---|---|---|
role | string | همیشه assistant. یک بار، در ابتدای stream، میرسد. | مدلهای 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های فراخوانی ابزار» را ببینید. | همه مدلها |
ابتدا استدلال میآید، سپس پاسخ، سپس هر فراخوانی ابزار. chunk را از روی کلیدهای delta آن بخوانید، نه از روی جایش در stream: یک delta میتواند role را همراه اولین تکه متن داشته باشد و آخری خالی است.
یک stream کامل همانطور که حرکت میکند. برای کوتاه ماندن خطوط، id، object، created و model در اینجا از هر chunk حذف شدهاند.
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های استدلال
مدلی که استدلال میکند، استدلال خود را پیش از پاسخ در delta.reasoning_content میفرستد. این دو متن را جدا نگه دارید: استدلال را بهصورت بخشی جداگانه و جمعشدنی نشان دهید، یا آن را نمایش ندهید.
مدلهای open-weight میزبانیشده وقتی reasoning_effort برابر off باشد استدلالی نمیفرستند، و وقتی درخواست response_format دارد هم نمیفرستند.
shannon-2-lite و shannon-2-pro هم پیش از پاسخ استدلال میکنند. در این endpoint، در همان مدت خطهای کامنت : thinking میفرستند و بعد پاسخ را.
reasoning_content در مدلهای تایپشده SDKهای OpenAI نیست. در Python آن را با getattr(delta, "reasoning_content", None) بخوانید؛ در JavaScript همان delta.reasoning_content مستقیم کار میکند.
deltaهای فراخوانی ابزار
فراخوانی ابزار کامل میرسد: برای هر فراخوانی یک chunk، که delta آن فراخوانی را با رشته کامل arguments دارد. index فراخوانیهای پاسخ را از 0 شمارهگذاری میکند. کدی که arguments را تکهتکه به هم میچسباند بدون تغییر کار میکند، چون فقط یک تکه را میچسباند.
{
"tool_calls": [
{
"index": 0,
"id": "call_3d9a7c1e5b2f4a60c8e1d7f09b24a6c5",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\":\"Paris\"}"
}
}
]
} بعد از فراخوانیها، آخرین chunk مقدار finish_reason را tool_calls دارد.
وقتی درخواست tools دارد، shannon-1.6-lite، shannon-1.6-pro و shannon-coder-1 متن پاسخ را بهصورت یک delta در پایان پاسخ میفرستند.
آخرین chunk
آخرین chunk پیش از data: [DONE] یک delta خالی، finish_reason و usage درخواست را همه در همان chunk دارد. هر stream آن را دارد؛ به 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 همراه endpoint توضیح داده شدهاند. Chat Completions
خطهای keep-alive
تا وقتی مدل مشغول کار است و هنوز چیزی برای ارسال ندارد، stream خطهای کامنت میفرستد. این خطها داده ندارند. پارسر 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 | تا وقتی مدل استدلال میکند. |
خطاها درون یک stream
وقتی stream شروع شد، وضعیتش 200 است، پس شکست داخل stream میرسد: یک chunk با عضو 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] بعد از chunk خطا، آخرین chunk با finish_reason و usage و data: [DONE] همچنان میآیند. هر chunk را برای 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 هنگام پیمایش، chunk خطا را بهصورت خطای API بالا میاندازند، پس حلقه را در مدیریت خطای معمول خود بپیچید.
کدهای وضعیت، بدنه خطا و اینکه چه چیزی را دوباره امتحان کنید صفحه خودشان را دارند. مدیریت خطا
بستن اتصال
بستن اتصال تحویل را متوقف میکند، نه درخواست را. مدل پاسخ را تا انتها مینویسد و درخواست طوری محاسبه میشود که انگار همه آن را خواندهاید. برای پرداخت کمتر، کمتر بخواهید: روی مدلی که max_tokens را اعمال میکند مقدار پایینتری بگذارید.
stream میتواند در مسیر رسیدن به شما هم زودتر تمام شود، برای مثال با قطع شبکه یا هنگام بهروزرسانی یک سرور. آنگاه بدون آخرین chunk و بدون data: [DONE] متوقف میشود. چنین پاسخی را ناقص در نظر بگیرید و درخواست را دوباره بفرستید.
خواندن stream بدون SDK
یک خواننده درست پنج کار میکند: خطاهایی را که پیش از stream میرسند به مدیریت خطای خود میدهد، بدنه را به خطها تقسیم میکند، خطهای کامنت را رد میکند، در data: [DONE] متوقف میشود و هر chunk را برای 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 یا بالاتر اجرا میشود.
streamها در قالبهای دیگر
endpointهای Responses و Messages هم stream میکنند. فریمهای آنها یک خط event: با نام رویداد و یک خط data: با JSON آن دارند. هر صفحه رویدادهای خود را به ترتیب فهرست کرده است.
| در stream | 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 |
| میزان مصرف میرسد در | آخرین chunk | response.completed | message_delta |
| stream پایان مییابد با | آخرین chunk، سپس data: [DONE] | response.completed | message_stop |