דילוג לתוכן
הזרמה

הזרמה

קבעו 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}")

איך 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:

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

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]

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 חתיכה אחר חתיכה עובד ללא שינוי, כי הוא מחבר חתיכה אחת.

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. לכל stream יש אותו; אין צורך ב-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 התשובה מכילה קריאה אחת או יותר לכלים. כל המודלים
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.

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 השיחה ארוכה מחלון ההקשר של המודל. קצרו אותה לפני שתשלחו שוב. נוסח ההודעה משתנה.

מודלי ה-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))

דוגמת Python משתמשת בחבילה requests. דוגמת JavaScript רצה על Node.js 18 ומעלה.

Streams של הפורמטים האחרים

גם ה-endpoints של Responses ו-Messages תומכים ב-streaming. ה-frames שלהם נושאים שורת event: עם שם האירוע ושורת data: עם ה-JSON שלו. כל דף מפרט את האירועים שלו לפי הסדר.

ב-stream Chat CompletionsResponsesMessages
Frames שורות data:שורות event: ו-data:שורות event: ו-data:
Reasoning מגיע ב delta.reasoning_contentresponse.reasoning_summary_text.deltathinking_delta
טקסט התשובה מגיע ב delta.contentresponse.output_text.deltatext_delta
Usage מגיע ב ה-chunk האחרוןresponse.completedmessage_delta
ה-stream מסתיים ב ה-chunk האחרון, ואחריו data: [DONE]response.completedmessage_stop