---
title: Стриминг
description: SSE в трёх диалектах шлюза — как включить поток, какие приходят события, чем кончается целый ответ и почему после первого байта вызов уже не переносят.
keywords: стриминг, sse, server-sent events, done, message_stop, response.completed, отмена
group: gateway
---

## Быстро {#quick keywords="stream, include_usage, curl, python, node"}

Добавьте `"stream": true` к тому же вызову — и ответ придёт как `text/event-stream`. На Chat Completions попросите ещё и расход: `stream_options.include_usage`.

:::code-group
```bash title=curl
curl -N https://api.kumorouter.com/v1/chat/completions \
  -H "Authorization: Bearer $KUMO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "<model>",
    "max_tokens": 128,
    "stream": true,
    "stream_options": { "include_usage": true },
    "messages": [{ "role": "user", "content": "Напиши хайку про задержку." }]
  }'
```
```python title=Python
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.kumorouter.com/v1",
    api_key=os.environ["KUMO_API_KEY"],
)

stream = client.chat.completions.create(
    model="<model>",
    max_tokens=128,
    stream=True,
    stream_options={"include_usage": True},
    messages=[{"role": "user", "content": "Напиши хайку про задержку."}],
)
for chunk in stream:
    for choice in chunk.choices:
        print(choice.delta.content or "", end="", flush=True)
```
```javascript title=Node
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api.kumorouter.com/v1",
  apiKey: process.env.KUMO_API_KEY,
});

const stream = await client.chat.completions.create({
  model: "<model>",
  max_tokens: 128,
  stream: true,
  stream_options: { include_usage: true },
  messages: [{ role: "user", content: "Напиши хайку про задержку." }],
});

for await (const chunk of stream) {
  process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}
```
:::

## Поток без завершающего кадра — это неудавшийся вызов {#terminal keywords="done, message_stop, response.failed, обрыв, усечение"}

Отказ, решённый **до** первого события, приходит обычной JSON-ошибкой со статусом — тем же конвертом, что и у неразбитого на события вызова, и в потоке при этом нет ничего. Сбой **после** первого события таким быть уже не может: строка статуса ушла раньше, и её не переписать.

Поэтому каждый диалект называет свой завершающий кадр, и целый ответ виден именно по нему.

:::matrix
| Диалект | Завершающий кадр | Если его не пришло |
| --- | --- | --- |
| Chat Completions | `[DONE]` | Поток просто кончился. Ответ усечён. |
| Anthropic Messages | `message_stop` | Пришло событие `error` этого протокола, и поток кончился без `message_stop`. |
| Responses | `response.completed` | Пришло `response.failed` с конвертом ошибки вместо готового тела. |
:::

Читайте именно это отсутствие. Клиент, который считает «соединение закрылось» синонимом «ответ дописан», отдаст дальше усечённый текст, и в полученных им байтах ничто не скажет обратного. Поток, кончившийся до завершающего кадра, — неудавшийся вызов, а не короткий.

Практическое правило: держите флаг «завершающий кадр видели» и после выхода из цикла проверяйте его первым, до того как отдать текст дальше.

## Что приходит в потоке {#events keywords="события, чанки, дельты, usage"}

### Chat Completions

События `chat.completion.chunk` в порядке прихода. У каждого `id`, `model` и `object` те же, что были бы у унарного ответа, а содержимое лежит в `choices[0].delta`: `role` — на первом чанке, дальше `content`, `refusal` или `tool_calls` кусками. `finish_reason` заявляется один раз, на чанке, который закрывает ответ, и до тех пор он `null`.

Если запрошен `stream_options.include_usage`, перед `[DONE]` придёт **чанк расхода**: событие с пустым `choices`, несущее только `usage`. На то, из чего платформа считает вызов, этот член не влияет — расход собирается всегда, и он решает лишь, покажет ли его ваш собственный поток.

```text title=Кадры
data: {"id":"chatcmpl-8f2b7e10c9","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}

data: {"id":"chatcmpl-8f2b7e10c9","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"Задержка"},"finish_reason":null}]}

data: {"id":"chatcmpl-8f2b7e10c9","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}

data: {"id":"chatcmpl-8f2b7e10c9","object":"chat.completion.chunk","choices":[],"usage":{"prompt_tokens":12,"completion_tokens":18,"total_tokens":30}}

data: [DONE]
```

### Anthropic Messages

Шесть именованных событий по порядку: `message_start` — открывающий конверт с личностью ответа и входной половиной расхода; `content_block_start`, `content_block_delta`, `content_block_stop` — на каждый блок; `message_delta` — закрывающие факты, `stop_reason` и расход целиком; `message_stop`. Кроме них приходят `ping` и `error`.

Дельта блока бывает двух видов: `text_delta` несёт кусок текста, `input_json_delta` — кусок аргументов вызова инструмента как частичный JSON в `partial_json`.

### Responses

Собственные именованные события: `response.created`, `response.output_item.added`, `response.output_text.delta`, `response.refusal.delta`, `response.function_call_arguments.delta`, `response.output_item.done` и ровно одно завершающее — `response.completed` или `response.failed`. Имя события повторяется в поле `event:` SSE-кадра, так что ветвиться можно на любом из двух.

Расход приходит на `response.completed`. `response.failed` несёт конверт ошибки этого протокола в члене `error`.

## Поток на диалекте Messages {#messages-example keywords="anthropic, sdk, поток, пример"}

```python title=Python
import os
from anthropic import Anthropic

client = Anthropic(
    base_url="https://api.kumorouter.com",
    api_key=os.environ["KUMO_API_KEY"],
)

with client.messages.stream(
    model="<model>",
    max_tokens=256,
    messages=[{"role": "user", "content": "Напиши хайку про задержку."}],
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)
```

## Первый байт решает всё {#first-byte keywords="переключение, канал, первый байт, повтор"}

До модели, как правило, есть больше одного канала, и отказ канала **до** того, как до вас дошла хоть часть ответа, переносит вызов на следующий канал той же модели.

После первого байта переноса нет и быть не может: вам нельзя выдать второй ответ посреди того, который вы уже читаете. Отсюда и берётся всё поведение потока — обрыв вместо ошибки, отсутствие кадра вместо конверта.

## Отмена {#cancellation keywords="отмена, отключение, резерв, disconnect"}

Закрыв соединение, вы отменяете вызов: платформа видит отключение и записывает его как ваш отказ ждать. Отменённый вызов на другой канал не переносится — вы его больше не ждёте.

Расход, который поставщик успел подтвердить, всё равно рассчитывается: гонка вашего отключения с приходом результата не делает работу бесплатной. Там, где подтверждать нечего, вызов освобождает то, что держал.

## Где стриминга нет {#no-stream keywords="эмбеддинги, изображения, count_tokens"}

- У эмбеддингов стриминга нет: эта поверхность отвечает векторами целиком.
- У генерации изображений члена `stream` нет вовсе.
- Подсчёт токенов к провайдеру не обращается и потому потока не даёт.

:::note
Стриминг — это доказанная возможность точной пары модели и канала, а не свойство протокола. Запрос со `stream: true` не поедет к паре, которая стриминг не доказала: он отвергается до обращения наверх. Что доказано у конкретной модели, видно на её карточке в каталоге.
:::

## Что дальше {#next keywords="ошибки, chat completions, messages, responses"}

:::cards
- [Ошибки](/ru/errors) — конверт отказа, коды ответа и переключение между каналами.
- [Chat Completions](/ru/chat-completions) — унарный вызов того же протокола.
- [Messages](/ru/messages) — диалект Anthropic целиком.
- [Responses](/ru/responses) — типизированные элементы вывода.
- [Каталог моделей](/ru/models) — какая модель что доказала.
:::
