Стриминг LLM API у разных провайдеров: SSE-форматы, буферизация токенов и что ломается при переключении
Практическое руководство по стримингу LLM API через OpenAI, Anthropic, DashScope и DeepSeek: форматы SSE-событий, буферизация токенов, обработка ошибок в потоке, стриминг tool-вызовов и нормализация на уровне gateway.
Стриминг LLM API у разных провайдеров: SSE-форматы, буферизация токенов и что ломается при переключении
Все крупные LLM API поддерживают стриминг через Server-Sent Events (SSE). Устанавливаете stream: true — и токены приходят инкрементально, а не одним финальным ответом. В теории просто. На практике OpenAI, Anthropic, DashScope и DeepSeek реализуют SSE достаточно по-разному, чтобы переключение провайдера — или маршрутизация через gateway — сломало ваш стриминг-клиент неочевидным образом.
Это руководство документирует конкретные различия. Мы протестировали стриминг-вывод каждого провайдера и зафиксировали, как выглядит проводной протокол: типы событий, формы чанков, сигналы завершения, поведение при ошибках в потоке и сериализация tool-вызовов. Если вы используете мульти-провайдерную архитектуру или рассматриваете её, это справочник, который мы хотели бы иметь, когда строили собственный слой нормализации стриминга.
OpenAI-совместимость означает, что провайдер предоставляет endpoint chat-completions, чей контракт запроса и ответа достаточно близок к API OpenAI, чтобы немодифицированный вызов OpenAI SDK работал после замены трёх значений: API key, base URL, название модели. Минимальная поверхность на практике —POST /v1/chat/completions с messages, model и потоковым ответом в форме OpenAI.
Как SSE-стриминг выглядит на практике
Все четыре провайдера возвращают Content-Type: text/event-stream и отправляют чанки, разделённые переносами строк. На этом сходство заканчивается.
Формат OpenAI Chat Completions
OpenAI Chat Completions стриминг отправляет строки data: без поля event:. Каждый чанк — JSON-объект с object: "chat.completion.chunk" и массивом choices, содержащим объект delta. Delta содержит role, content, tool_calls или refusal — только инкрементальную часть, не полное сообщение. Источник: Руководство OpenAI по стримингу, получено 2026-07-31.
data: {"id":"chatcmpl-abc","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}
data: {"id":"chatcmpl-abc","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"Привет"},"finish_reason":null}]}
data: {"id":"chatcmpl-abc","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: [DONE]
Ключевые детали:
- Сигнал завершения: строка
data: [DONE]означает конец потока. Это не валидный JSON. - Usage: включается только при передаче
stream_options: {"include_usage": true}. Чанк с usage имеет пустой массивchoices. - Rate-limit заголовки: отправляются в HTTP-заголовках ответа (
x-ratelimit-limit-requests,x-ratelimit-remaining-tokensи т.д.), а не в потоке. Источник: Исследование стриминга LLM API Саймона Уиллисона, получено 2026-07-31.
Более новый Responses API от OpenAI использует типизированные семантические события (response.created, response.output_text.delta, response.completed) вместо формата чанков Chat Completions, но Chat Completions остаётся форматом, который эмулируют OpenAI-совместимые провайдеры.
Формат Anthropic Messages
Anthropic использует в SSE оба поля — event: и data: — ключевое отличие от OpenAI. Поток следует жизненному циклу: message_start → content_block_start → content_block_delta (повторяется) → content_block_stop → message_delta → message_stop. Источник: Документация Anthropic по стримингу, получено 2026-07-31.
event: message_start
data: {"type":"message_start","message":{"id":"msg_01X","role":"assistant","content":[],"usage":{"input_tokens":25,"output_tokens":1}}}
event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}
event: ping
data: {"type":"ping"}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Привет"}}
event: content_block_stop
data: {"type":"content_block_stop","index":0}
event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":15}}
event: message_stop
data: {"type":"message_stop"}
Ключевые детали:
- Нет
[DONE]: Anthropic завершает поток событиемevent: message_stop. Нет сигналаdata: [DONE]. - Ping-события: Anthropic отправляет
event: pingдля поддержания соединения. Клиент, ожидающий только строкиdata:, сломается. - Usage в двух местах: входные токены — в
message_start; выходные — вmessage_delta. - Индексация блоков контента: поле
indexподдерживает несколько блоков контента (text, tool_use, thinking) в одном ответе. OpenAI используетchoices[0].indexдля похожей, но структурно другой цели. - Rate-limit заголовки: используют префикс
anthropic-ratelimit-*, а неx-ratelimit-*.
DashScope (OpenAI-совместимый режим)
DashScope предоставляет OpenAI-совместимый endpoint по адресу https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions. При установке stream: true формат ответа повторяет чанки OpenAI Chat Completions: строки data: с объектами chat.completion.chunk, завершение — data: [DONE]. Источник: Alibaba Cloud Model Studio — OpenAI-совместимость, получено 2026-07-31.
Различия тонкие:
- Заголовок аутентификации: OpenAI-совместимый режим DashScope использует
Authorization: Bearer <api-key>, идентично OpenAI. - Токены мышления/рассуждения: при вызове моделей Qwen с включённым мышлением (например,
qwen3.7-maxсenable_thinking: true) токены рассуждения стримятся отдельно. DashScope оборачивает их в ту же структуруdelta, но с полемreasoning_contentрядом сcontent. Этого поля нет в спецификации OpenAI. - Размер чанков: в наших тестах DashScope отправляет чуть больше токенов на SSE-чанк, чем OpenAI — обычно 2–4 токена на чанк против 1 токена на чанк у OpenAI для текстовых моделей. Это означает меньше HTTP-фреймов для того же объёма вывода, что может влиять на восприятие time-to-first-token в UI.
Формат DeepSeek
API DeepSeek явно совместим с OpenAI, а также поддерживает endpoint в формате Anthropic по адресу https://api.deepseek.com/anthropic. Для OpenAI-совместимого пути стриминг следует тому же паттерну data: + chat.completion.chunk + data: [DONE]. Источник: Документация DeepSeek API, получено 2026-07-31.
Замеченные особенности:
- Контент рассуждений: как и DashScope, DeepSeek стримит токены рассуждений через поле
reasoning_contentв delta при включённом режиме мышления. Это то же расширение, что и у DashScope, его нет в спецификации OpenAI. - Поддержка двух форматов: DeepSeek — единственный first-party провайдер, нативно поддерживающий оба формата SSE (OpenAI и Anthropic) из одного API. Путь
/anthropicвозвращает события жизненного цикла в стиле Anthropic. - Индикаторы cache-попаданий: DeepSeek может включать
cache_creation_input_tokensиcache_read_input_tokensв чанк usage, аналогично полям prompt caching у Anthropic.
Различия буферизации токенов
Провайдеры не сбрасывают токены с одинаковой скоростью. Это важно для пользовательского опыта — UI чата, рендерящий посимвольно, выглядит отзывчивым, но UI, получающий 3 секунды тишины, а потом 50 токенов разом, кажется зависшим.
| Провайдер | Типично токенов на чанк | Поведение TTFT | Примечания |
|---|---|---|---|
| OpenAI | 1 токен | Быстрый TTFT, стабильная подача | Самый консистентный потокенный стриминг |
| Anthropic | 1–3 токена | Быстрый TTFT, иногда пакетами | Ping-события заполняют тишину |
| DashScope | 2–4 токена | Умеренный TTFT, крупные пакеты | Модели Qwen группируют агрессивнее |
| DeepSeek | 1–2 токена | Варьируется, зависит от модели | Reasoning-модели: долгий TTFT, затем быстрый вывод |
Для reasoning-моделей у всех провайдеров ожидайте длительную паузу перед первым видимым токеном контента — модель сначала генерирует цепочку рассуждений. Некоторые провайдеры стримят токены рассуждений (DashScope, DeepSeek через reasoning_content); другие скрывают их до начала финального ответа.
Обработка ошибок в потоке
Когда что-то идёт не так после установления SSE-соединения и начала потока токенов, каждый провайдер обрабатывает это по-разному. Это самая сложная часть мульти-провайдерного стриминга — ваш HTTP-код ответа уже 200.
OpenAI
OpenAI отправляет финальный чанк с полем error или резко закрывает соединение. В SSE-потоке Chat Completions нет стандартного типа события ошибки. Если модель срабатывает на фильтр контента в процессе генерации, последний delta может содержать finish_reason: "content_filter" вместо "stop". Если сервер падает, TCP-соединение обрывается без data: [DONE] — клиент должен обрабатывать неполные потоки.
Anthropic
Anthropic может отправить event: error с payload data: {"type":"error","error":{"type":"overloaded_error","message":"..."}}. Ошибка приходит как стандартное SSE-событие, поэтому клиенты, слушающие типы event:, могут чисто её перехватить. Затем соединение закрывается. Источник: Документация Anthropic по ошибкам API, получено 2026-07-31.
DashScope
OpenAI-совместимый стриминг DashScope следует паттерну OpenAI: ошибки в потоке редки, проявляются как обрыв соединения или повреждённые чанки. Нативный API DashScope имеет более структурированные события ошибок, но совместимый режим жертвует этим ради совместимости с OpenAI на проводном уровне.
DeepSeek
OpenAI-совместимый путь DeepSeek следует поведению ошибок OpenAI. Anthropic-совместимый путь следует паттерну событий ошибок Anthropic.
Правило для production: всегда реализуйте timeout и детектор неполного потока. Если вы получаете чанки, но не видите сигнал завершения (data: [DONE] или event: message_stop) в течение timeout, считайте ответ неудачным и логируйте частичный вывод для отладки.
Стриминг tool-вызовов: самая сложная задача нормализации
Стриминг tool-вызовов означает получение имени функции и JSON-аргументов посимвольно. Это уже непросто с одним провайдером. Между провайдерами различия множатся.
OpenAI
Tool-вызовы стримятся через delta.tool_calls[i].function.name (отправляется один раз) и delta.tool_calls[i].function.arguments (инкрементальные строковые фрагменты). Необходимо конкатенировать фрагменты arguments и парсить полный JSON только после finish_reason: "tool_calls". Несколько tool-вызовов могут чередоваться по индексу.
{"delta":{"tool_calls":[{"index":0,"id":"call_abc","type":"function","function":{"name":"get_weather","arguments":""}}]}}
{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"lo"}}]}}
{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"cation\":"}}]}}
{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\"Paris\"}"}}]}}
Anthropic
Anthropic стримит tool-вызовы как блок контента с type: "tool_use". Событие content_block_start несёт {"type":"tool_use","id":"toolu_abc","name":"get_weather","input":{}}, затем события content_block_delta несут фрагменты {"type":"input_json_delta","partial_json":"..."}. Паттерн конкатенации JSON тот же, но оболочка полностью отличается от OpenAI.
DashScope и DeepSeek
Оба следуют формату стриминга tool-вызовов OpenAI при использовании OpenAI-совместимого endpoint. Anthropic-совместимый endpoint DeepSeek следует формату стриминга tool-use от Anthropic. DashScope не предоставляет Anthropic-совместимый endpoint.
Задача gateway: стриминг-gateway, принимающий любой upstream-формат и выдающий консистентный downstream-формат, должен поддерживать состояние для каждого соединения: какие блоки контента открыты, какие аргументы tool-вызовов собираются, какова семантика завершения исходного провайдера. Проект LLM-Rosetta (Аргоннская национальная лаборатория, апрель 2026) формализовал это как схему из 10 типов потоковых событий с управлением состоянием. Источник: Статья LLM-Rosetta, получено 2026-07-31.
Content-Type и жизненный цикл соединения
| Провайдер | Заголовок Content-Type | Keep-alive | Сигнал закрытия |
|---|---|---|---|
| OpenAI | text/event-stream; charset=utf-8 | Управляется сервером | data: [DONE] |
| Anthropic | text/event-stream; charset=utf-8 | Управляется сервером + ping | event: message_stop |
| DashScope | text/event-stream; charset=utf-8 | Управляется сервером | data: [DONE] |
| DeepSeek (OpenAI) | text/event-stream; charset=utf-8 | Управляется сервером | data: [DONE] |
| DeepSeek (Anthropic) | text/event-stream; charset=utf-8 | Управляется сервером | event: message_stop |
Все провайдеры используют chunked transfer encoding HTTP/1.1 или data frames HTTP/2. SSE-соединение — это долгоживущий HTTP-ответ, а не WebSocket. Responses API от OpenAI также предлагает WebSocket-режим для постоянных соединений, но это отдельный транспорт.
Браузерный EventSource API не может потреблять ни один из этих endpoint, потому что EventSource поддерживает только GET-запросы; LLM API требуют POST. Используйте fetch() с потоковым body reader или библиотеку вроде @microsoft/fetch-event-source.
Gateway-стриминг: нормализация разнородных форматов
Когда TheRouter маршрутизирует стриминг-запрос через настроенных провайдеров, gateway должен нормализовать upstream SSE в консистентный downstream-контракт. Мы маршрутизируем OpenAI-совместимые запросы через настроенных провайдеров и поддерживаем маршрутизацию и fallback провайдеров/моделей там, где это поддерживается в production.
Задача нормализации имеет три уровня:
- Оболочка событий: преобразование жизненного цикла Anthropic
event:+data:в чанки OpenAI-стиля только сdata:, или наоборот. Это включает маппингmessage_start→ первый чанк сrole,content_block_delta→delta.content,message_delta→ финальный чанк сfinish_reason. - Поля-расширения: удалить или сохранить провайдер-специфичные поля вроде
reasoning_content,cache_read_input_tokensили расположениеusageу Anthropic. Downstream-клиенты не должны ломаться на неожиданных полях, но и не должны полагаться на поля, которые отправляет только один upstream-провайдер. - Семантика завершения: обеспечить, чтобы downstream всегда получал ожидаемый сигнал завершения вне зависимости от upstream. Если upstream — Anthropic, а downstream ожидает формат OpenAI, gateway должен отправить
data: [DONE]после обработкиevent: message_stop.
Для более широкого обзора сравнения провайдеров по ценам, моделям и API см. наше сравнение провайдеров LLM API и руководство по OpenAI-совместимым API-провайдерам.
Пример кода: минимальный кросс-провайдерный стриминг-клиент
Этот Python-клиент обрабатывает и OpenAI-стиль, и Anthropic-стиль SSE-потоков. Он намеренно минимален — production-реализация требует логики повторов, обработки timeout и надлежащего backpressure.
import httpx
import json
def stream_openai_compatible(base_url: str, api_key: str, model: str, messages: list):
"""Стриминг от любого OpenAI-совместимого endpoint (OpenAI, DashScope, DeepSeek)."""
with httpx.stream(
"POST",
f"{base_url}/chat/completions",
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
json={"model": model, "messages": messages, "stream": True,
"stream_options": {"include_usage": True}},
timeout=60.0,
) as response:
buffer = ""
for line in response.iter_lines():
if not line or line.startswith(":"):
continue
if line == "data: [DONE]":
break
if line.startswith("data: "):
chunk = json.loads(line[6:])
delta = chunk.get("choices", [{}])[0].get("delta", {})
if content := delta.get("content"):
yield content
if reasoning := delta.get("reasoning_content"):
yield f"[thinking] {reasoning}"
def stream_anthropic(api_key: str, model: str, messages: list):
"""Стриминг от нативного API Anthropic."""
with httpx.stream(
"POST",
"https://api.anthropic.com/v1/messages",
headers={
"x-api-key": api_key,
"anthropic-version": "2023-06-01",
"Content-Type": "application/json",
},
json={"model": model, "messages": messages, "stream": True, "max_tokens": 4096},
timeout=60.0,
) as response:
for line in response.iter_lines():
if not line:
continue
if line.startswith("event: "):
event_type = line[7:]
if event_type == "message_stop":
break
continue
if line.startswith("data: "):
data = json.loads(line[6:])
if data.get("type") == "content_block_delta":
delta = data.get("delta", {})
if delta.get("type") == "text_delta":
yield delta.get("text", "")
О том, как работает fallback-маршрутизация, когда стриминг-провайдер отказывает посреди запроса, читайте в нашем руководстве по fallback-маршрутизации LLM API.
Production-подводные камни
Буферизация proxy и CDN
Reverse proxy (nginx, Cloudflare, AWS ALB) могут буферизовать SSE-ответы и доставлять их крупными пакетами вместо потокенной передачи. Это уничтожает стриминг-опыт. Решения:
- nginx:
proxy_buffering off;и заголовок ответаX-Accel-Buffering: no - Cloudflare: заголовок
cf-no-transformили отключение буферизации ответов в зоне - AWS ALB: ALB не поддерживает SSE нативно во всех конфигурациях — рассмотрите NLB или прямое подключение
Настройка timeout
SSE-соединения долгоживущие. Стандартные timeout HTTP-клиентов (30 с) убьют стриминг-запросы с длинными выводами. Разделяйте read timeout (на чанк) и connection timeout:
# httpx: общий timeout vs read timeout
timeout = httpx.Timeout(connect=10.0, read=120.0, write=10.0, pool=10.0)
Backpressure
Если ваш клиент обрабатывает токены медленнее, чем сервер их отправляет, TCP receive buffer заполняется и сервер в конечном счёте блокируется на send. Для большинства LLM API это не практическая проблема — генерация токенов медленнее сетевой передачи — но batch-стриминг или кэшированные ответы могут перегрузить медленных клиентов.
Keep-alive и переподключение
SSE-соединения могут обрываться незаметно из-за сетевых проблем. Ping-события Anthropic помогают быстро обнаружить мёртвое соединение. Для OpenAI-стиля потоков без ping реализуйте read timeout: если данные не приходят в течение N секунд и поток не завершён, переподключайтесь. Учтите, что LLM API обычно не поддерживают возобновление потока с точки обрыва — обрыв соединения означает новый запрос.
Чек-лист: подключение нового стриминг-endpoint провайдера
При интеграции стриминг-API нового LLM-провайдера проверьте каждый пункт:
- Формат событий: провайдер использует только
data:(стиль OpenAI) илиevent:+data:(стиль Anthropic)? - Сигнал завершения:
data: [DONE],event: message_stopили что-то другое? - Путь извлечения контента: где text delta?
choices[0].delta.content?delta.text_delta.text? Что-то провайдер-специфичное? - Стриминг tool-вызовов: следует паттерну OpenAI
tool_calls[i].function.argumentsили Anthropicinput_json_delta? - Отчёт об usage: usage включён в поток? Только по запросу? В первом чанке, последнем или обоих?
- Поля-расширения: провайдер добавляет нестандартные поля вроде
reasoning_content,cache_read_input_tokensили custom metadata? - Сигнализация ошибок: как провайдер сигнализирует об ошибках в потоке? Структурированное событие? Обрыв соединения? Повреждённый чанк?
- Механизм keep-alive: провайдер отправляет ping-события или соединение молчит между генерациями токенов?
- Размер чанков: провайдер отправляет отдельные токены или пакетами? Это влияет на воспринимаемую задержку.
О различиях ошибок и кодов статуса у разных провайдеров в не-стриминг-контексте читайте в наших сравнении rate limit и сравнении провайдеров.
FAQ
Можно ли использовать браузерный EventSource API для потребления стриминг-endpoint LLM?
Нет. EventSource API поддерживает только GET-запросы. Все стриминг API для LLM требуют POST. Используйте fetch() с ReadableStream reader или библиотеку вроде @microsoft/fetch-event-source. Подробности интеграции провайдеров — в нашем руководстве OpenAI-совместимые API-провайдеры.
Все OpenAI-совместимые провайдеры стримят идентично OpenAI?
По большей части — да, для базового пути текстового стриминга. Формат строк data: и завершение data: [DONE] согласованы. Различия проявляются в полях-расширениях (reasoning_content у DashScope и DeepSeek), поведении отчётов об usage и размерах чанков. Стриминг tool-вызовов — область с наиболее тонкими различиями.
Что случится, если переключиться с OpenAI на Anthropic посреди проекта?
Ваш стриминг-клиент сломается. Anthropic использует полностью иной жизненный цикл событий (message_start / content_block_delta / message_stop) с именованными типами event:. Вам нужна либо новая реализация клиента, либо gateway, нормализующий формат Anthropic в OpenAI-совместимые чанки.
Как обрабатывать reasoning-токены в стриминге?
DashScope и DeepSeek стримят reasoning-токены через поле reasoning_content в объекте delta. Anthropic стримит extended thinking как отдельный блок контента с type: "thinking". OpenAI не стримит reasoning-токены в Chat Completions — они потребляются внутренне. Ваш клиент должен корректно обрабатывать поле reasoning_content (игнорировать, если неожиданное; отображать, если UI это поддерживает).
Существует ли стандарт кросс-провайдерной нормализации стриминга?
Проект LLM-Rosetta (апрель 2026) предложил hub-and-spoke IR с 10 типами потоковых событий как формальный подход. На практике большинство gateway и SDK (LiteLLM, Portkey, TheRouter) реализуют собственные слои нормализации. Формат чанков OpenAI Chat Completions — де-факто стандарт, на который ориентируются совместимые провайдеры.