ข้ามไปยังเนื้อหา
สตรีมมิง

สตรีมมิง

ตั้ง 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}")

สตรีมถูกจัดเฟรมอย่างไร

  • คำตอบมีสถานะ 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:

JSON
{
  "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 ในที่นี้

200 text/event-stream
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 ทีละส่วนยังทำงานได้ตามเดิม เพราะต่อเพียงส่วนเดียว

delta
{
  "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

JSON
{
  "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

200 text/event-stream
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))

ตัวอย่าง Python ใช้แพ็กเกจ requests ตัวอย่าง JavaScript รันบน Node.js 18 ขึ้นไป

สตรีมของรูปแบบอื่น

endpoint Responses และ Messages ก็สตรีมได้เช่นกัน เฟรมของทั้งสองมีบรรทัด event: ที่ระบุชื่อ event และบรรทัด data: ที่เป็น JSON แต่ละหน้าแสดงรายการ event ตามลำดับ

ในสตรีม Chat CompletionsResponsesMessages
เฟรม บรรทัด data:บรรทัด event: และ data:บรรทัด event: และ data:
การให้เหตุผลมาถึงใน delta.reasoning_contentresponse.reasoning_summary_text.deltathinking_delta
ข้อความคำตอบมาถึงใน delta.contentresponse.output_text.deltatext_delta
usage มาถึงใน chunk สุดท้ายresponse.completedmessage_delta
สตรีมจบด้วย chunk สุดท้าย แล้ว data: [DONE]response.completedmessage_stop