halfoff.devДокументация
Получить ключ
Models API

Потоковая передача

Потоковый вывод Claude, GPT и Gemini в нативном формате SSE каждого вендора. Как скрываются ранние сбои, как проявляются зависания и сколько стоит отменённый поток.

Потоковая передача работает так, как её задумал каждый вендор: server-sent events в собственном формате вендора, переданные вам байт в байт. Шлюз читает поток лишь для подсчёта usage и чтобы защитить вас от отказывающих каналов. Ваш существующий код разбора потока менять не нужно.

Включение потоковой передачи#

APIКак запросить потокПоследнее событие
Anthropic Messages"stream": true в телеevent: message_stop
OpenAI Chat Completions"stream": true в телеdata: [DONE]
GeminiPOST /v1beta/models/{model}:streamGenerateContent?alt=sseСобытие, у кандидата которого есть finishReason

Код SDK для каждого API есть в руководствах: Anthropic, OpenAI, Gemini.

Первый токен удерживается, сбои до него повторяются#

Иногда канал принимает запрос, а затем сдаётся, не успев ничего выдать, например отправив message_start, а потом ошибку перегрузки. Чтобы это скрыть, шлюз удерживает начало каждого потока, пока не придёт первая порция настоящего содержимого (текст, thinking или вызов инструмента). Канал, отказавший до этого момента, ничего вам не отправил, поэтому шлюз тихо пересылает запрос на другой канал, и вы получаете один чистый поток с самого начала.

Медленные старты обрабатываются так же. Для моделей Claude без extended thinking канал, не выдавший ни одного токена за 10 секунд (плюс 1 секунда на каждые 10K токенов промпта), заменяется другим, если только он не последний оставшийся. При включённом thinking модель может размышлять сколько нужно. Непотоковые вызовы подчиняются тому же правилу.

Для вашего клиента это означает:

  • Строка статуса и заголовки приходят вместе с первым токеном: время до первого байта — это время до первого токена.
  • Если все каналы отказывают до первого токена, вы получаете обычную HTTP-ошибку в формате вашего API, а не оборванный поток.
  • Удержание длится не более 15 секунд. Модель, которая молча думает дольше, начинает передачу в этот момент, и с этого мига запрос закрепляется за своим каналом.
  • Давайте ответу на старт не менее 30 секунд. Запрос также может сначала несколько секунд простоять в очереди за мощностью; см. Лимиты запросов и переключение.

Примеры потоков#

SSE
event: message_start
data: {"type":"message_start","message":{"id":"msg_01Qe4n","type":"message","role":"assistant","model":"claude-sonnet-5-5","content":[],"stop_reason":null,"usage":{"input_tokens":12,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":1}}}

event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Red, green, blue."}}

event: content_block_stop
data: {"type":"content_block_stop","index":0}

event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":7}}

event: message_stop
data: {"type":"message_stop"}

Когда идущий поток обрывается#

После первого токена запрос принадлежит своему каналу и больше не может быть перенесён. Прервать его всё же могут две вещи.

Тишина. Если канал ничего не отправляет 3 минуты, шлюз закрывает поток событием ошибки в формате вашего API:

SSE
event: error
data: {"type":"error","error":{"type":"api_error","message":"Upstream stopped responding mid-stream"}}

Ошибка, возникшая на канале, например перегруженная модель. Вы получаете нейтральное событие ошибки в своём формате, а не формулировку самого канала: overloaded_error (у OpenAI rate_limit_exceeded, у Gemini RESOURCE_EXHAUSTED), если канал занят, иначе api_error (у Gemini UNAVAILABLE) с сообщением The model provider returned an error. Please retry.

В обоих случаях поток заканчивается без обычного завершающего события (message_stop, data: [DONE] или finishReason). Считайте любой поток без него незавершённым: сохраните или отбросьте частичный текст, а если нужен полный ответ, повторите весь запрос. Anthropic SDK и OpenAI SDK выбрасывают исключение, когда видят событие ошибки.

Досрочная остановка потока#

Закройте соединение, чтобы остановить генерацию: выйдите из цикла или вызовите stream.close() / controller.abort(). Шлюз останавливает модель и тарифицирует по половине прайсовой цены только то, что было сгенерировано к этому моменту:

  • Вход: число, о котором поток уже сообщил (Anthropic присылает его в начале), либо около одного токена на четыре символа вашего запроса, если ничего не сообщалось.
  • Выход: большее из уже сообщённого числа и примерно одного токена на четыре символа текста, уже отправленного вам. Досрочно завершённые потоки редко сообщают итоговое число выходных токенов, так что обычно применяется оценка.

Поток, закрытый после зависания, тарифицируется так же. Каждый отменённый или зависший поток появляется в Логах со своим списанием. См. также Модели и цены.