Перейти до вмісту
Стримінг

Стримінг

Задайте stream у true, і відповідь надходитиме як server-sent events, поки модель її пише: спершу міркування, потім відповідь, потім останній чанк із причиною завершення та usage. Використовуйте це щоразу, коли людина чекає на текст, і для довгих відповідей.

POST https://api.shannon-ai.com/v1/chat/completions

SDK OpenAI віддають вам чанки по одному. Читайте кожен чанк за тим, що він несе: шматок міркувань, шматок відповіді, виклик інструмента або кінець відповіді.

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.
  • Кожна подія — це один рядок, що починається з data: і за яким іде порожній рядок. Текст після data: — один чанк JSON. Рядків event: чи id: немає.
  • Рядок, що починається з двокрапки, як-от : ping, — це коментар, який тримає з'єднання відкритим. Пропускайте його.
  • Стрім завершується data: [DONE]. Стрім, що зупинився без нього, неповний.

Чанки за порядком

Кожен чанк — це об'єкт 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
    }
  ]
}

Усі чанки одного стріму мають однакові id, created і model. choices завжди містить один запис. Його delta містить нове, а finish_reason дорівнює null до останнього чанка.

Ключ у 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 call deltas. Усі моделі

Спершу міркування, потім відповідь, потім виклики інструментів. Читайте чанк за ключами в його delta, а не за його місцем у стрімі: delta може містити role разом із першим шматком тексту, а останній порожній.

Увесь стрім у тому вигляді, в якому він передається. Щоб рядки були короткими, id, object, created і model тут пропущено в кожному чанку.

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.reasoning_content перед відповіддю. Тримайте ці два тексти окремо: показуйте міркування як окрему частину, що згортається, або не показуйте зовсім.

Хостовані open-weight моделі не надсилають міркувань, коли reasoning_effort дорівнює off, і коли запит має response_format.

shannon-2-lite і shannon-2-pro теж міркують перед відповіддю. На цьому ендпоінті вони, поки міркують, надсилають рядки-коментарі : thinking, а потім відповідь.

reasoning_content немає в типізованих моделях SDK OpenAI. У Python читайте його через getattr(delta, "reasoning_content", None); у JavaScript delta.reasoning_content працює як є.

Дельти викликів інструментів

Виклик інструмента надходить цілим: один чанк на виклик, delta якого містить виклик із повним рядком arguments. index нумерує виклики відповіді від 0. Код, що склеює arguments по шматках, працює без змін, бо склеює один шматок.

delta
{
  "tool_calls": [
    {
      "index": 0,
      "id": "call_3d9a7c1e5b2f4a60c8e1d7f09b24a6c5",
      "type": "function",
      "function": {
        "name": "get_weather",
        "arguments": "{\"city\":\"Paris\"}"
      }
    }
  ]
}

Після викликів останній чанк має finish_reason tool_calls.

Коли запит має tools, shannon-1.6-lite, shannon-1.6-pro і shannon-coder-1 надсилають текст відповіді однією дельтою наприкінці відповіді.

Останній чанк

Останній чанк перед data: [DONE] має порожню delta, finish_reason та usage запиту, усе в одному чанку. Його має кожен стрім; 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 описано разом з ендпоінтом. 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, тож збій надходить усередині стріму: чанк із членом 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]

Після чанка помилки все одно йдуть останній чанк із finish_reason і usage та data: [DONE]. Перевіряйте кожен чанк на 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 викидають чанк помилки як помилку API під час ітерації, тож обгорніть цикл у звичну обробку помилок.

Коди статусів, тіло помилки та що повторювати мають окрему сторінку. Обробка помилок

Закриття з'єднання

Закриття з'єднання зупиняє доставку, а не запит. Модель дописує відповідь до кінця, і запит тарифікується так, ніби ви прочитали все. Щоб платити менше, просіть менше: задайте нижчий max_tokens на моделі, яка його застосовує.

Стрім може також обірватися дорогою до вас, наприклад через збій мережі або під час оновлення сервера. Тоді він зупиняється без останнього чанка й без data: [DONE]. Вважайте таку відповідь неповною та надішліть запит знову.

Читання стріму без SDK

Правильний читач робить п'ять речей: передає помилки, що надійшли до стріму, у свою обробку помилок, розбиває тіло на рядки, пропускає рядки-коментарі, зупиняється на data: [DONE] і перевіряє кожен чанк на 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 або новішій.

Стріми інших форматів

Ендпоінти Responses і Messages теж стрімлять. Їхні кадри несуть рядок event: з назвою події та рядок data: з її JSON. Кожна сторінка перелічує свої події за порядком.

У стрімі Chat CompletionsResponsesMessages
Кадри рядки data:рядки event: і data:рядки event: і data:
Міркування надходять у delta.reasoning_contentresponse.reasoning_summary_text.deltathinking_delta
Текст відповіді надходить у delta.contentresponse.output_text.deltatext_delta
Usage надходить у Останній чанкresponse.completedmessage_delta
Стрім завершується Останній чанк, потім data: [DONE]response.completedmessage_stop