สตรีมมิง
ตั้ง stream เป็น true แล้วคำตอบจะมาถึงเป็น server-sent events ขณะที่โมเดลเขียน: การให้เหตุผลก่อน แล้วคำตอบ แล้ว chunk สุดท้ายที่มี finish reason และ usage ใช้เมื่อมีคนรอข้อความอยู่ และสำหรับคำตอบที่ยาว
POST https://api.shannon-ai.com/v1/chat/completions
OpenAI SDK ให้ chunk ทีละรายการ ให้อ่านแต่ละ chunk ตามสิ่งที่มี: ส่วนของการให้เหตุผล ส่วนของคำตอบ การเรียก tool หรือจุดสิ้นสุดของคำตอบ
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 - แต่ละ event เป็นหนึ่งบรรทัดที่ขึ้นต้นด้วย
data:และตามด้วยบรรทัดว่าง ข้อความหลังdata:คือ JSON chunk หนึ่งรายการ ไม่มีบรรทัดevent:หรือid: - บรรทัดที่ขึ้นต้นด้วยเครื่องหมายทวิภาค เช่น
: pingเป็นคอมเมนต์ที่เปิดการเชื่อมต่อค้างไว้ ให้ข้ามไป - สตรีมจบด้วย
data: [DONE]สตรีมที่หยุดโดยไม่มีบรรทัดนี้ถือว่าไม่สมบูรณ์
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 ของสตรีมเดียวกันมี id, created และ model เหมือนกัน choices มีหนึ่งรายการเสมอ delta ของมันมีสิ่งที่เพิ่มขึ้นใหม่ และ finish_reason เป็น null จนถึง chunk สุดท้าย
| คีย์ใน 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 ที่สมบูรณ์หนึ่งรายการ ดู Tool call delta | ทุกโมเดล |
การให้เหตุผลมาก่อน แล้วคำตอบ แล้วการเรียก tool (ถ้ามี) ให้อ่าน chunk ตามคีย์ใน delta ไม่ใช่ตามตำแหน่งในสตรีม: delta อาจมี role พร้อมข้อความส่วนแรก และอันสุดท้ายว่าง
สตรีมทั้งหมดตามที่ส่งผ่านเครือข่าย เพื่อให้บรรทัดสั้น 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 ไม่อยู่ในโมเดลแบบมีชนิดของ OpenAI SDK ใน Python ให้อ่านด้วย getattr(delta, "reasoning_content", None); ใน JavaScript ใช้ delta.reasoning_content ได้ตามเดิม
Delta ของการเรียก tool
การเรียก tool มาถึงครบในครั้งเดียว: หนึ่ง 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 เดียวกัน ทุกสตรีมมี chunk นี้ คุณไม่ต้องใช้ 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 | คำตอบมีการเรียก tool หนึ่งรายการขึ้นไป | ทุกโมเดล |
length | คำตอบถูกตัดที่ขีดจำกัดเอาต์พุต | ตระกูล Shannon 3, shannon-1.6-lite, shannon-1.6-pro, shannon-coder-1 |
ฟิลด์ของ usage อธิบายไว้พร้อมกับ endpoint 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 ความล้มเหลวจึงมาถึงภายในสตรีม: 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 | บทสนทนายาวกว่า context window ของโมเดล ให้ย่อก่อนส่งอีกครั้ง ข้อความของข้อผิดพลาดแตกต่างกันไป |
โมเดล open-weight ที่โฮสต์ส่งสมาชิก error ที่มีเพียง type และ message
OpenAI SDK สำหรับ Python และ JavaScript จะยก chunk ข้อผิดพลาดเป็นข้อผิดพลาดของ API ขณะที่คุณวนอ่าน ดังนั้นให้ครอบลูปด้วยการจัดการข้อผิดพลาดตามปกติของคุณ
รหัสสถานะ body ของข้อผิดพลาด และสิ่งที่ควรลองใหม่ อยู่ในหน้าของตัวเอง การจัดการข้อผิดพลาด
การปิดการเชื่อมต่อ
การปิดการเชื่อมต่อหยุดการส่ง แต่ไม่ได้หยุดคำขอ โมเดลเขียนคำตอบจนจบและคำขอถูกคิดเงินเหมือนคุณอ่านครบทั้งหมด หากต้องการจ่ายน้อยลง ให้ขอน้อยลง: ตั้ง max_tokens ให้ต่ำลงบนโมเดลที่ใช้ค่านี้
สตรีมอาจจบก่อนกำหนดระหว่างทางมายังคุณ เช่น เครือข่ายขาดหรือขณะเซิร์ฟเวอร์กำลังอัปเดต สตรีมจะหยุดโดยไม่มี chunk สุดท้ายและไม่มี data: [DONE] ให้ถือว่าคำตอบนั้นไม่สมบูรณ์และส่งคำขอใหม่
การอ่านสตรีมโดยไม่ใช้ SDK
ตัวอ่านที่ถูกต้องทำห้าอย่าง: ส่งข้อผิดพลาดที่มาถึงก่อนสตรีมไปยังตัวจัดการข้อผิดพลาด แบ่ง body เป็นบรรทัด ข้ามบรรทัดคอมเมนต์ หยุดที่ 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 ขึ้นไป
สตรีมของรูปแบบอื่น
endpoint Responses และ Messages ก็สตรีมได้เช่นกัน เฟรมของทั้งสองมีบรรทัด event: ที่ระบุชื่อ event และบรรทัด data: ที่เป็น JSON แต่ละหน้าแสดงรายการ event ตามลำดับ
| ในสตรีม | 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 มาถึงใน | chunk สุดท้าย | response.completed | message_delta |
| สตรีมจบด้วย | chunk สุดท้าย แล้ว data: [DONE] | response.completed | message_stop |