Към съдържанието
Стрийминг

Стрийминг

Задайте 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 Едно пълно извикване на инструмент. Вижте Делти на извикване на инструмент. Всички модели

Първо идват разсъжденията, после отговорът, после евентуалните извиквания на инструменти. Четете фрагмента по ключовете в неговата 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, чието търсене е намерило нещо, последният фрагмент носи и sources, а един фрагмент преди него носи белезите за цитиране като 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