Потоковая передача ответа: когда нужен stream: true
stream: true не для каждого случая. Разбираем, как читать поток событий, где он реально нужен и какие ошибки чаще всего в нём прячутся.
4 мин · · Обновлено · разработка · стриминг
stream: true добавляют в запрос почти рефлекторно — «модели же стримят токены, значит, надо стримить». Но потоковый ответ — это дополнительная сложность на стороне клиента: разбор SSE-событий, накопление частичных чанков, обработка обрыва посреди потока. Если фронтенд не показывает текст по мере генерации, а просто ждёт полный ответ, стриминг только добавляет код без выгоды.
Что такое стриминг на практике
Без stream: true сервер отвечает одним JSON-объектом после того, как модель полностью сгенерировала ответ. С stream: true сервер отправляет ответ по частям через Server-Sent Events (SSE) — по мере того как модель генерирует токены, а не после завершения.
Обычный запрос:
curl https://api.zukko.pro/v1/chat/completions \
-H "Authorization: Bearer fetch_ваш_ключ" \
-H "Content-Type: application/json" \
-d '{"model":"claude-sonnet-5","messages":[{"role":"user","content":"Напиши короткое стихотворение"}]}'Потоковый запрос — то же тело плюс "stream": true:
curl https://api.zukko.pro/v1/chat/completions \
-H "Authorization: Bearer fetch_ваш_ключ" \
-H "Content-Type: application/json" \
-d '{"model":"claude-sonnet-5","stream":true,"messages":[{"role":"user","content":"Напиши короткое стихотворение"}]}' \
--no-bufferФлаг --no-buffer в curl важен для наблюдения за потоком в реальном времени — иначе терминал может буферизовать вывод и показать всё разом.
Формат событий: OpenAI-стиль
В /v1/chat/completions каждое событие — это строка data: {...}, заканчивающаяся data: [DONE]:
data: {"choices":[{"delta":{"content":"Ветер"}}]}
data: {"choices":[{"delta":{"content":" гонит"}}]}
data: {"choices":[{"delta":{"content":" листья"}}]}
data: [DONE]
Клиент должен накапливать delta.content из каждого события в единый текст:
import requests
resp = requests.post(
"https://api.zukko.pro/v1/chat/completions",
headers={"Authorization": "Bearer fetch_ваш_ключ"},
json={"model": "claude-sonnet-5", "stream": True,
"messages": [{"role": "user", "content": "Напиши короткое стихотворение"}]},
stream=True,
)
full_text = ""
for line in resp.iter_lines():
if not line or not line.startswith(b"data: "):
continue
payload = line[len(b"data: "):]
if payload == b"[DONE]":
break
import json
chunk = json.loads(payload)
delta = chunk["choices"][0]["delta"].get("content", "")
full_text += delta
print(delta, end="", flush=True)Формат событий: Anthropic-стиль
В /v1/messages события структурированы иначе — с типами message_start, content_block_delta, message_stop и другими:
event: message_start
data: {"type":"message_start","message":{...}}
event: content_block_delta
data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"Ветер"}}
event: content_block_delta
data: {"type":"content_block_delta","delta":{"type":"text_delta","text":" гонит"}}
event: message_stop
data: {"type":"message_stop"}
Обязательный заголовок anthropic-version: 2023-06-01 нужен и в потоковом режиме — без него запрос вернёт ошибку ещё до начала стрима:
curl https://api.zukko.pro/v1/messages \
-H "Authorization: Bearer fetch_ваш_ключ" \
-H "anthropic-version: 2023-06-01" \
-H "Content-Type: application/json" \
-d '{"model":"claude-sonnet-5","max_tokens":300,"stream":true,"messages":[{"role":"user","content":"Напиши короткое стихотворение"}]}' \
--no-bufferКод на стороне клиента при этом должен фильтровать по type внутри data, а не только по наличию content_block_delta — интересен именно text_delta, остальные типы событий несут служебную информацию (начало блока, статистику использования токенов, окончание сообщения).
Когда стриминг действительно нужен
Чат-интерфейс с видимым набором текста. Если пользователь видит, как ответ печатается по мере генерации — классический UX чат-ботов — стриминг обязателен, иначе пользователь будет смотреть на пустой экран несколько секунд при длинном ответе.
Длинные ответы с большим max_tokens. Чем длиннее ожидаемый ответ, тем больше времени модель тратит на полную генерацию. Без стриминга клиент просто ждёт HTTP-ответ целиком — при max_tokens в несколько тысяч это может быть заметная пауза без обратной связи.
Возможность прерывания в интерфейсе. Стриминг позволяет клиенту перестать читать ответ раньше, если пользователь передумал или ответ уже содержит нужную информацию — интерфейс освобождается, не дожидаясь полной генерации. Но на расход это не влияет: обрыв стрима не останавливает списание — шлюз дочитывает ответ провайдера и списывает всю генерацию. Ограничивать расход нужно через max_tokens, а не через разрыв соединения.
Когда стриминг не нужен
Серверная обработка без UI. Если ответ модели используется программно — парсится, кладётся в базу, передаётся дальше в пайплайн — пользователь не видит процесс генерации, и стриминг только усложняет код парсинга без всякой пользы.
Короткие ответы. Классификация в одно слово, оценка от 1 до 10, структурированный JSON на пару полей — генерация занимает доли секунды, разница между стримингом и обычным ответом не заметна.
Строгий JSON-парсинг. Если ответ должен быть валидным JSON, который парсится целиком после получения, накопление по кусочкам не даёт преимущества — парсер всё равно ждёт полный объект, прежде чем сможет его разобрать.
Обрыв потока — частая путаница с лимитами
Если поток обрывается на середине без явной ошибки, это почти всегда таймаут клиента или сети, а не лимит по токенам — стоит проверить max_tokens в запросе и таймаут HTTP-клиента (у многих HTTP-библиотек по умолчанию стоит таймаут в 30–60 секунд, которого не хватает на длинную генерацию). Учтите, что обрыв на стороне клиента не отменяет списание: шлюз всё равно дочитывает ответ провайдера и списывает всю генерацию. Проверить, сколько стоил оборванный запрос, можно в кабинете или через GET /v1/usage — там появится запись с model, input_tokens, output_tokens и cost_cents.
Заголовки в потоковом ответе
Заголовков стоимости X-Zukko-Cost-Micros и X-Fetch-Balance-Cents у потоковых ответов нет — они приходят только в обычных JSON-ответах (stream: false). Точная стоимость фиксируется только после завершения генерации, а заголовки потока отправляются раньше. Для потоковых запросов расход смотрят в кабинете («Использование»), через GET /v1/usage (история общая на аккаунт) или по балансу после запроса:
resp = requests.post(url, headers=headers, json=body, stream=True)
for line in resp.iter_lines():
... # читаем поток до конца
balance = requests.get("https://api.zukko.pro/v1/balance", headers=headers).json()
print("balance after, cents:", balance["balance_cents"])Если для каждого запроса нужна точная стоимость прямо в ответе, используйте обычный (нестриминговый) режим и читайте X-Zukko-Cost-Micros (микродоллары, 1 000 000 = $1).
Итог
stream: true стоит включать там, где есть живой интерфейс, показывающий текст по мере генерации, или где ответ достаточно длинный, чтобы пауза без обратной связи была заметна. Для серверной обработки, коротких ответов и строгого JSON — обычный запрос проще в реализации и не теряет ничего по функциональности.
zukko
Платите в 25×
меньше
за Claude Code & ChatGPT
Один ключ fetch_* и base URL api.zukko.pro — в Cursor, Claude Code, Codex и любом SDK.
Похожие статьи
- Промпт-инъекция: что это и как защитить бота и агентаОбъясняем, как работает промпт-инъекция через пользовательский ввод и чужие документы, и какие меры снижают ущерб: изоляция данных, права инструментов, лимиты ключа.
- Свой прокси к нейросети на Node.js: не показывать API-ключ во фронтендеНебольшой сервер на Express между вашим фронтендом и API нейросети: ключ остаётся на сервере, а модели, длина запроса и частота ограничены.
- DeepSeek API на Python: запросы, стриминг, JSON и обработка ошибокГотовый код для DeepSeek V4.1 Flash и V4 Pro на OpenAI SDK: запрос, стриминг, разбор JSON и аккуратная обработка ошибок.
- Промпт-инжиниринг: примеры и 12 готовых шаблонов для работыДвенадцать шаблонов промптов для рабочих задач: что в них написано, почему именно так и сколько стоит один вызов по API.