API · Стриминг

Ответ по мере генерации

Стрим — это обычный HTTP-ответ с типом text/event-stream, который открыт всё время генерации. Прокси его не буферизует: кусок доходит до вас сразу, а не пачкой в конце.

Как включить#

bash
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, разбирать её не нужно.
text
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 есть только в первом куске вызова:

text
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

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

javascript
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 в каждом куске. Обработчик, который смотрит только на код ответа, примет оборванную генерацию за успешную и покажет пользователю обрезанный текст как готовый.