Ответ по мере генерации
Стрим — это обычный HTTP-ответ с типом text/event-stream, который открыт всё время генерации. Прокси его не буферизует: кусок доходит до вас сразу, а не пачкой в конце.
Как включить#
curl -N https://nirastudio.org/v1/chat/completions \
-H "Authorization: Bearer $NIRA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "anthropic/claude-sonnet-4.6",
"messages": [{ "role": "user", "content": "Считай до трёх" }],
"stream": true,
"stream_options": { "include_usage": true }
}'Заголовки ответа: Content-Type: text/event-stream; charset=utf-8 и Cache-Control: no-cache, no-transform. Второй важен не меньше первого — он запрещает промежуточным прокси собирать поток обратно в один ответ.
Порядок кусков#
В OpenAI-протоколе все события безымянные: строки data: {…}, разделённые пустой строкой. Порядок постоянный:
- кусок с
delta.role: "assistant"— открывает ответ, текста в нём нет; - много кусков с
delta.content— по несколько символов; - если модель вызывает инструменты — куски с
delta.tool_calls; - кусок с пустой
deltaи заполненнымfinish_reason; - кусок с
usage, если просилиinclude_usage; - строка
data: [DONE]— не JSON, разбирать её не нужно.
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant"}}]}
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"один"}}]}
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[],"usage":{"prompt_tokens":12,"completion_tokens":5,"total_tokens":17}}
data: [DONE]usage массив choices пустой. Код, который берёт choices[0] без проверки, падает именно здесь — и только когда включили include_usage.Инструменты в стриме#
Аргументы функции приходят по частям и склеиваются по index. Имя и id есть только в первом куске вызова:
data: {"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_…","type":"function","function":{"name":"get_weather","arguments":""}}]}}]}
data: {"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"city"}}]}}]}
data: {"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\":\"Москва\"}"}}]}}]}
data: {"choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}]}Разбирать arguments можно только после finish_reason: в середине это обрывок JSON, а не JSON.
В протоколе Anthropic#
Там события именованные — message_start, content_block_delta, message_stop и так далее; [DONE] не используется, конец потока — событие message_stop. Полный список — в Anthropic Messages.
Читать в коде#
Python
import os
from openai import OpenAI
client = OpenAI(base_url="https://nirastudio.org/v1", api_key=os.environ["NIRA_API_KEY"])
stream = client.chat.completions.create(
model="anthropic/claude-sonnet-4.6",
messages=[{"role": "user", "content": "Считай до трёх"}],
stream=True,
stream_options={"include_usage": True},
)
for chunk in stream:
if not chunk.choices: # кусок с usage — choices пустой
continue
piece = chunk.choices[0].delta.content
if piece:
print(piece, end="", flush=True)Node.js
const stream = await client.chat.completions.create({
model: "anthropic/claude-sonnet-4.6",
messages: [{ role: "user", content: "Считай до трёх" }],
stream: true,
});
for await (const chunk of stream) {
const piece = chunk.choices[0]?.delta?.content;
if (piece) process.stdout.write(piece);
}Библиотека сама разбирает data:, склеивает куски и останавливается на [DONE]. Разбирать SSE руками стоит только там, где библиотеки нет, — см. Node.js и Python.
Отмена и обрывы#
| Ситуация | Что происходит |
|---|---|
| Клиент закрыл соединение | Шлюз прекращает запрос к провайдеру. Списываются токены, посчитанные до обрыва |
| Провайдер упал посреди ответа | Приходит кусок с полем error. Код ответа остаётся 200 — он ушёл в самом начале |
| Долгая пауза без текста | Нормально для рассуждающих моделей: они думают до первого токена. Таймаут клиента стоит ставить от 120 с |
error в каждом куске. Обработчик, который смотрит только на код ответа, примет оборванную генерацию за успешную и покажет пользователю обрезанный текст как готовый.