Стриминг ответов

Со стримингом ответ приходит по частям, по мере генерации: первые слова видны через доли секунды, а не после того, как модель допишет всё. Стриминг работает в Chat Completions, Responses API, Anthropic Messages и API для Cursor и стоит столько же, сколько обычный запрос.

Как включить

Добавьте в запрос "stream": true. Ответ придёт потоком server-sent events (SSE). Флаг -N не даёт curl копить вывод, и события печатаются сразу:

curl -N https://infergate.ru/api/v1/chat/completions \
  -H "Authorization: Bearer $INFERGATE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "openai/gpt-4o-mini",
    "messages": [{"role": "user", "content": "Напиши короткое стихотворение"}],
    "stream": true
  }'

SDK OpenAI разбирает поток сам:

import os
from openai import OpenAI

client = OpenAI(
    base_url="https://infergate.ru/api/v1",
    api_key=os.environ["INFERGATE_API_KEY"],
)
stream = client.chat.completions.create(
    model="openai/gpt-4o-mini",
    messages=[{"role": "user", "content": "Напиши короткое стихотворение"}],
    stream=True,
)
for chunk in stream:
    if chunk.choices and chunk.choices[0].delta.content:
        print(chunk.choices[0].delta.content, end="", flush=True)
    if chunk.usage:
        print("\n", chunk.usage.cost_rub, "₽")

Chat Completions

Каждое событие — строка data: с объектом chat.completion.chunk. Текст приходит кусками в choices[0].delta.content, вызовы инструментов — в choices[0].delta.tool_calls. Поток заканчивается строкой data: [DONE].

data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant","content":"Над"}}]}

data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":" рекой"}}]}

data: [DONE]

Расход токенов и стоимость приходят в последнем фрагменте перед [DONE], в поле usage. В этом фрагменте может не быть текста, а массив choices — оказаться пустым: проверяйте его, прежде чем читать choices[0]. Строки, которые начинаются с двоеточия (: keep-alive), — служебные, их можно пропускать; SDK делают это сами.

Так же устроен поток на адресе для Cursor.

Responses API

События типизированы, как в Responses API OpenAI: у каждого есть поле type. Обычный порядок такой: response.created, response.in_progress, response.output_item.added, response.content_part.added, затем куски текста в поле delta событий response.output_text.delta, после них response.output_text.done, response.content_part.done, response.output_item.done и в конце response.completed.

Событие response.completed содержит весь ответ целиком, а в response.usage — расход токенов и стоимость запроса.

Anthropic Messages

Поток в формате Anthropic: у каждого события есть строка event: с типом и строка data: с данными. Порядок такой:

  • message_start — начало ответа;
  • content_block_start, затем content_block_delta и content_block_stop — для каждого блока ответа. Текст — в delta.text, рассуждения — в delta.thinking, аргументы вызова инструмента — кусками JSON в delta.partial_json;
  • message_delta — причина остановки (stop_reason) и расход токенов в usage;
  • message_stop — конец ответа.

Стоимости в этом формате нет — её показывает журнал запросов в личном кабинете.

Ошибки и обрывы

  • Ошибки, найденные до начала генерации, приходят обычным HTTP-ответом с кодом ошибки, а не потоком. Например, если max_tokens больше, чем допускает модель, запрос получит 422 с кодом invalid_max_tokens. Остальные коды — на странице «Ошибки».
  • Если генерация сорвалась, когда поток уже шёл, он закончится без [DONE] (в Anthropic Messages — без message_stop). Считайте такой ответ неполным и повторите запрос. Если поток оборвался до того, как пришёл расход, запрос не оплачивается.
  • Если вы сами закроете соединение раньше времени, генерация, как правило, не остановится: модель допишет ответ, и он будет оплачен целиком.

Повтор потокового запроса с заголовком Idempotency-Key приходит одним JSON, а не потоком.

Где стриминга нет

Эмбеддинги, реранкинг, решения и распознавание речи возвращают результат одним ответом, поле stream на них не влияет. Генерация изображений с "stream": true получает 422. Видео создаётся асинхронной задачей. Синтез речи отдаёт аудиофайл, который начинает приходить, как только готовы первые данные.