Перейти к содержимому
Потоковая передача

Потоковая передача

Задайте stream равным true, и ответ придет как server-sent events по мере того, как его пишет модель: сначала reasoning, затем ответ, затем последний чанк с причиной завершения и данными об использовании. Используйте это, когда текст ждет человек, и для длинных ответов.

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

OpenAI SDK отдают чанки по одному. Читайте каждый чанк по тому, что он несет: фрагмент 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}")

Как устроены фреймы потока

  • Ответ имеет статус 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. Приходит один раз, в начале потока. Размещенные модели с открытыми весами, shannon-2-lite, shannon-2-pro
reasoning_content string Фрагмент рассуждений модели. Склеивайте фрагменты по порядку. Размещенные модели с открытыми весами, семейство Shannon 3, shannon-1.6-pro, shannon-coder-1
content string Фрагмент ответа. Склеивайте фрагменты по порядку. Все модели
tool_calls array Один полный вызов инструмента. См. «Дельты вызова инструмента». Все модели

Сначала идет reasoning, затем ответ, затем вызовы инструментов, если они есть. Читайте чанк по ключам его 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]

Дельты reasoning

Модель, которая рассуждает, отправляет рассуждения в delta.reasoning_content перед ответом. Держите два текста раздельно: показывайте reasoning как отдельную сворачиваемую часть или не показывайте вовсе.

Размещенные модели с открытыми весами не отправляют reasoning, когда reasoning_effort равен off, а также когда в запросе есть response_format.

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

reasoning_content нет в типизированных моделях OpenAI SDK. В 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
  }
}

У размещенных моделей с открытыми весами в 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 Размещенные модели с открытыми весами Каждые 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 Разговор длиннее контекстного окна модели. Сократите его, прежде чем отправлять снова. Текст сообщения может различаться.

Размещенные модели с открытыми весами отправляют член error только с type и message.

OpenAI SDK для 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:
Reasoning приходит в delta.reasoning_contentresponse.reasoning_summary_text.deltathinking_delta
Текст ответа приходит в delta.contentresponse.output_text.deltatext_delta
Данные об использовании приходят в Последний чанкresponse.completedmessage_delta
Поток заканчивается Последний чанк, затем data: [DONE]response.completedmessage_stop