Для AI-агентов: markdown этой страницы — /docs-content/ai/chat/streaming.md индекс документации — /llms.txt
Потоковая передача (Server-Sent Events)
При stream: true ответ модели приходит потоком событий по мере генерации токенов, а не одним куском в конце. Первый байт доходит до клиента за секунды независимо от полной длины ответа.
Заголовки ответа: Content-Type: text/event-stream, Cache-Control: no-cache, Connection: keep-alive. Каждое событие — строка data: {JSON}\n\n, завершающее событие — data: [DONE]\n\n.
Формат событий
data: {"id":"chatcmpl-a9f6128818355f17","object":"chat.completion.chunk","model":"bitrix/bitrixgpt-5.5","choices":[{"index":0,"delta":{"role":"assistant","content":"CRM"}}]}
data: {"id":"chatcmpl-a9f6128818355f17","object":"chat.completion.chunk","model":"bitrix/bitrixgpt-5.5","choices":[{"index":0,"delta":{"content":" — это"}}]}
data: {"id":"chatcmpl-a9f6128818355f17","object":"chat.completion.chunk","model":"bitrix/bitrixgpt-5.5","choices":[{"index":0,"finish_reason":"stop","delta":{}}],"usage":{"prompt_tokens":10,"completion_tokens":15,"total_tokens":25}}
data: [DONE]
usage приходит в последнем событии перед [DONE]. Накапливайте delta.content из всех событий для получения полного текста ответа.
При запросе с function tools пустой ход или завершение вызова без самого вызова сопровождается английским объяснением в отдельном событии delta.content с finish_reason: null, перед единственным завершающим finish_reason: "stop". Если текст уже был, объяснение отделено пустой строкой. Это подсказка продолжить с объявленными инструментами: попытка вызвать необъявленное имя могла быть отброшена до платформы. Ответ остаётся HTTP 200; usage и [DONE] не меняются, объяснение не добавляет оплачиваемых токенов. Для tool_choice: "none", структурированного ответа, реального вызова и хода только с рассуждением, завершившегося stop, такая подсказка не добавляется.
Пример обработки потока
const response = await fetch('https://vibecode.bitrix24.tech/v1/chat/completions', {
method: 'POST',
headers: {
'X-Api-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'bitrix/bitrixgpt-5.5',
messages: [{ role: 'user', content: 'Расскажи про CRM' }],
stream: true,
}),
})
const reader = response.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split('\n')
buffer = lines.pop() ?? ''
for (const line of lines) {
if (!line.startsWith('data: ')) continue
const payload = line.slice(6)
if (payload === '[DONE]') return
const chunk = JSON.parse(payload)
const delta = chunk.choices?.[0]?.delta?.content
if (delta) process.stdout.write(delta)
}
}
Выбор режима под нагрузкой
В синхронном режиме (stream: false) полный ответ модели собирается на сервере и уходит клиенту одним ответом — первый байт приходит вместе с последним. Для модели с длинной фазой рассуждения bitrix/bitrixgpt-5.5-thinking на длинном контексте и с развёрнутым ответом генерация занимает десятки секунд. Если клиентский таймаут короче полного времени генерации, соединение закрывается без единого полученного байта — например, cURL error 28: Operation timed out, 0 bytes received. На коротких запросах генерация укладывается в таймаут, и эффект не проявляется.
В потоковом режиме (stream: true) сервер отправляет статус 200 и заголовки сразу после начала обработки, а события идут по мере генерации токенов, включая фазу рассуждения. Для объёмных запросов и моделей с рассуждением используйте stream: true — клиентский таймаут по приёму первого байта тогда не срабатывает.
Ориентиры по клиентскому таймауту:
- Потоковый режим (
stream: true) — задавайте таймаут на приём первого события с запасом на обдумывание модели. Модель с рассуждением тратит несколько секунд до первого токена, значение от 30 секунд покрывает эту фазу с запасом. - Синхронный режим (
stream: false) — таймаут по всему запросу должен покрывать полное время генерации, а не только сетевой обмен. Для объёмного запроса к модели с рассуждением это десятки секунд. Кроме того, при ошибке или таймауте провайдера синхронный запрос повторяется автоматически, и тогда финальный ответ или ошибка приходят через несколько минут. Поэтому значений 9-15 секунд недостаточно: задавайте таймаут по всему запросу с запасом на такие повторы — несколько минут — либо переходите на потоковый режим, где таймаут по приёму первого байта снимает проблему.
Известные особенности
Резервной модели в потоке нет. При сбое провайдера синхронный запрос повторяется с моделью по умолчанию, а в потоковом режиме клиент получает ошибку в последнем событии перед data: [DONE].
Ошибки приходят внутри потока. Статус 200 уже отправлен, когда платформа узнаёт о перегрузке или обрыве генерации, поэтому служебное событие вида data: { "error": { "code": "pool_exhausted", "retryAfter": 5 } } приходит перед data: [DONE]. Читайте поток до конца и проверяйте поле error.
Незавершённый структурированный ответ тоже приходит событием ошибки. Если в запросе был response_format, а полного JSON модель не выдала и повтор не обнаружен, перед data: [DONE] приходит data: { "error": { "code": "structured_output_truncated", "message": "...", "type": "invalid_request_error" } }. В этом событии только три поля — code, message и type. Полей finishReason, param и suggestedMaxTokens, которые несёт синхронный ответ 422, здесь нет. Текст message различается по причине завершения: генерация оборвана по finish_reason: "length" — предлагается повторить запрос с увеличенным max_tokens, поток закончился без причины завершения — ответ может быть неполным, модель завершилась сама с другой причиной — разбираемого JSON нет, рекомендуется модель без рассуждения. Подробнее — гарантированный JSON-ответ.
Неразбираемый структурированный ответ приходит отдельным событием. Если модель на запрос с response_format ответила непустым текстом, который не разбирается как JSON-объект или массив, перед data: [DONE] приходит data: { "error": { "code": "structured_output_invalid_json", "message": "...", "type": "invalid_request_error" } }. Набор полей события тот же — code, message и type. Фрагменты ответа к этому моменту уже переданы, поэтому собранное нужно отбросить, а статус потока остаётся 200 по протоколу. Повтор с увеличенным max_tokens результат не меняет. Восстановление из служебного канала reasoning_content здесь не выполняется, в отличие от пустого ответа: документ, добавленный после уже переданных фрагментов, склеился бы с ними в буфере клиента. Подробнее — гарантированный JSON-ответ.
Зацикленное рассуждение обрывается событием reasoning_repetition. Если модель, ещё не начав ответ, повторяет в reasoning_content одни и те же фразы по кругу, платформа прекращает генерацию, не дожидаясь срока обслуживания и лимита max_tokens, и присылает data: { "error": { "code": "reasoning_repetition", "message": "...", "type": "invalid_request_error" } } перед data: [DONE]. Ошибка не повторяемая: тот же запрос с тем же уровнем рассуждения зациклится снова — повторите его с выключенным рассуждением (reasoning_effort: "none"), увеличение max_tokens не поможет. Уже переданные фрагменты рассуждения остаются у клиента. Платформа ловит повтор одних и тех же фраз, а не перебор разных формулировок, и по умолчанию (repetition_guard не передан или "off") для раннего обрыва смотрит только на рассуждение до начала ответа: повторяющийся ответ в content (например, массив одинаковых объектов) не обрывается. Если модель присылает один и тот же текст сразу в оба канала, решает то, как он приходит клиенту: у модели с рассуждением этот текст приходит в content и доходит как ответ; если он приходит только как reasoning_content, его повтор обрывается так же, как рассуждение. Исключение — response_format: {"type": "json_object"}, когда модель присылает сам ответ каналом reasoning_content как JSON-документ (рассуждение начинается с { или [): тогда обрыв откладывается, пока восстановление возможно, и решает восстановление — полный документ приходит как ответ, а если восстановить не удалось, приходит reasoning_repetition. Если документ слишком длинный, чтобы его восстановить, повтор обрывает поток сразу. Без response_format исключения нет: ответ, пришедший только в reasoning_content, при зацикливании обрывается событием reasoning_repetition — восстановление ответа из рассуждения выполняется только для json_object.
У потока есть общий срок обслуживания. Кроме тайм-аута простоя у вызова теперь есть предельное время целиком: не уложился — поток завершается событием data: { "error": { "code": "ai_deadline_exceeded", "type": "rate_limit_exceeded", "retryable": true, "retryAfter": <секунды> } } перед data: [DONE]. Отличие от тайм-аута простоя в том, что этот срок срабатывает даже при непрерывно идущих токенах — он про длительность всего ответа, а не про паузу в нём. Свой, более короткий бюджет можно назвать заголовком запроса X-AI-Deadline-Ms; разбор — на странице Лимиты и перегрузка.
Зависший ответ обрывается по тайм-ауту простоя. Если модель отдала заголовки, но затем замолчала в середине ответа и не присылает новых данных дольше окна ожидания, платформа прерывает вызов и присылает data: { "error": { "code": "stream_idle_timeout", "type": "server_error", "retryable": true, "retryAfter": <секунды> } } перед data: [DONE]. Ошибка повторяемая — повторите запрос с учётом retryAfter. Непрерывный поток токенов (в том числе токенов рассуждений у «думающих» моделей) сбрасывает таймер простоя и под тайм-аут не попадает.
Тем же событием заканчивается и поток, который не начался. Если апстрим принял запрос, но не отдал заголовки ответа за отведённый бюджет, вызов завершается тем же повторяемым stream_idle_timeout перед data: [DONE], а не молчанием. Так ведут себя модели всех поставщиков: установка потока идёт одной попыткой с общим бюджетом, поэтому ожидание не удваивается.
Успешный поток заканчивается событием с finish_reason. Если модель закончила ответ, но не прислала завершающего события, платформа сама добавляет перед data: [DONE] событие с пустым delta и finish_reason: "tool_calls", когда в ответе были вызовы инструментов, или finish_reason: "stop" в остальных случаях. Если соединение с моделью оборвалось раньше, чем пришла причина завершения, ответ может быть неполным: перед data: [DONE] приходит data: { "error": { "code": "stream_incomplete", "type": "server_error", "retryable": true, "retryAfter": <секунды> } }. Ошибка повторяемая — повторите запрос с учётом retryAfter.
Повтор в оборванном JSON тоже даёт reasoning_repetition. При отсутствии repetition_guard или значении "off", с json_object или json_schema, если генерация завершилась по finish_reason: "length", непустой незавершённый JSON проверяется на повтор. При обнаруженном повторе приходят код reasoning_repetition и сообщение про JSON, без совета отключать рассуждение, без param и suggestedMaxTokens. Это эвристическая диагностика: она может сработать и на законном повторяющемся массиве, которому не хватило бюджета. Проверьте, ожидаемы ли повторы; если да, проверьте лимит токенов, иначе уточните запрос или смените модель. Проверка выполняется после завершения генерации и не сокращает ожидание. Короткая обрезка без обнаруженного повтора и целый JSON при length сохраняют structured_output_truncated; законный повторяющийся массив при нормальном завершении приходит целиком. В потоке отказ приходит одним событием error с полями code, message, hint, type перед data: [DONE]; уже переданные фрагменты content нужно отбросить. HTTP-статус остаётся 200.
Ранний отказ при повторении JSON
С явным repetition_guard: "abort" платформа проверяет нормализованный content до завершения генерации. Нужны json_object/json_schema, отсутствие объявленных инструментов и модель с поддержкой потока. Управляющие запросы и запросы завершения Cowork, а также объявления об исчерпании квоты исключены. Несовместимые запросы дают обычный JSON 400 invalid_request с param: "repetition_guard" до открытия потока, резервирования и обращения к модели. Подробности, эвристические ограничения и пример SDK — JSON-ответ.
При обнаружении повтора активный запрос к модели отменяется, а при уже открытом потоке клиент получает ровно эти два события при HTTP 200:
data: {"error":{"message":"Repeated JSON content was detected and this request was stopped early.","type":"invalid_request_error","code":"reasoning_repetition","hint":"Disable repetition_guard for expected repeated content; otherwise revise the request or use another model."}}
data: [DONE]
Отбросьте накопленный частичный ответ. Отказ не ждёт finish_reason: "length" и не добавляет выдуманную причину завершения. Он не содержит retryable, retryAfter, param, suggestedMaxTokens, сырой текст ответа или приватные идентификаторы провайдера. После него нет повтора, восстановления ответа или перехода на другую модель. Отмена соединения не гарантирует немедленного освобождения GPU.
Эвристика может отказать на законном повторяющемся префиксе массива. Полный JSON в момент проверки исключается, фактические вызовы инструментов отключают проверку, защита рассуждения не меняется. Для ожидаемых повторов оставляйте опцию выключенной ("off" или отсутствие). По умолчанию повторяющийся ответ по-прежнему доходит до терминальной проверки.