הזרמה
קבעו stream ל-true והתשובה מגיעה כ-server-sent events בזמן שהמודל כותב אותה: קודם reasoning, אחר כך התשובה, ולבסוף chunk אחרון עם סיבת הסיום וה-usage. השתמשו בו בכל פעם שאדם ממתין לטקסט, ובתשובות ארוכות.
POST https://api.shannon-ai.com/v1/chat/completions
ה-SDK של OpenAI נותנים לכם את ה-chunks אחד אחד. קראו כל chunk לפי מה שהוא נושא: חתיכת 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
}' איך stream ממוסגר
- לתשובה יש סטטוס
200והכותרותcontent-type: text/event-streamו-cache-control: no-cache. - כל אירוע הוא שורה אחת שמתחילה ב-
data:ואחריה שורה ריקה. הטקסט אחריdata:הוא chunk אחד של JSON. אין שורותevent:אוid:. - שורה שמתחילה בנקודתיים, כמו
: ping, היא הערה ששומרת את החיבור פתוח. דלגו עליה. - ה-stream מסתיים ב-
data: [DONE]. stream שנעצר בלעדיו אינו שלם.
Chunks לפי הסדר
כל 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
}
]
} כל ה-chunks של stream אחד נושאים את אותם id, created ו-model. choices מכיל תמיד פריט אחד. ה-delta שלו מכיל את החדש, ו-finish_reason הוא null עד ה-chunk האחרון.
| מפתח ב-delta | סוג | תיאור | נשלח על ידי |
|---|---|---|---|
role | string | תמיד assistant. הוא מגיע פעם אחת, בתחילת ה-stream. | מודלי open-weight מתארחים, shannon-2-lite, shannon-2-pro |
reasoning_content | string | חתיכה מה-reasoning של המודל. חברו את החתיכות לפי הסדר. | מודלי open-weight מתארחים, משפחת Shannon 3, shannon-1.6-pro, shannon-coder-1 |
content | string | חתיכה מהתשובה. חברו את החתיכות לפי הסדר. | כל המודלים |
tool_calls | array | קריאה שלמה אחת לכלי. ראו Tool call deltas. | כל המודלים |
קודם reasoning, אחר כך התשובה, ואחר כך קריאות לכלים. קראו 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] Reasoning deltas
מודל שמנמק שולח את ה-reasoning שלו ב-delta.reasoning_content לפני התשובה. שמרו את שני הטקסטים נפרדים: הציגו את ה-reasoning כחלק נפרד שאפשר לכווץ, או השמיטו אותו.
מודלי ה-open-weight המתארחים אינם שולחים reasoning כש-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 עובד כמות שהוא.
Tool call deltas
קריאה לכלי מגיעה שלמה: 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 בזמן שאתם עוברים על ה-stream, ולכן עטפו את הלולאה בטיפול השגיאות הרגיל שלכם.
לקודי סטטוס, גוף השגיאה ומה לנסות שוב יש דף משלהם. טיפול בשגיאות
סגירת החיבור
סגירת החיבור עוצרת את המסירה, לא את הבקשה. המודל כותב את התשובה עד סופה והבקשה מחויבת כאילו קראתם את כולה. כדי לשלם פחות, בקשו פחות: קבעו 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 ומעלה.
Streams של הפורמטים האחרים
גם ה-endpoints של Responses ו-Messages תומכים ב-streaming. ה-frames שלהם נושאים שורת event: עם שם האירוע ושורת data: עם ה-JSON שלו. כל דף מפרט את האירועים שלו לפי הסדר.
| ב-stream | Chat Completions | Responses | Messages |
|---|---|---|---|
| Frames | שורות data: | שורות event: ו-data: | שורות event: ו-data: |
| Reasoning מגיע ב | delta.reasoning_content | response.reasoning_summary_text.delta | thinking_delta |
| טקסט התשובה מגיע ב | delta.content | response.output_text.delta | text_delta |
| Usage מגיע ב | ה-chunk האחרון | response.completed | message_delta |
| ה-stream מסתיים ב | ה-chunk האחרון, ואחריו data: [DONE] | response.completed | message_stop |