Streaming API считается завершённым только после terminal event, предусмотренного конкретным протоколом. Закрытый сокет, timeout клиента или последний текстовый фрагмент этого не доказывают. При обрыве сохраните уже полученные события, request ID и состояние операции. Повторяйте запрос лишь после проверки побочных эффектов: stream нельзя универсально продолжить с последнего токена, а слепой retry может второй раз вызвать tool.
Нормальное завершение или настоящий обрыв
Server-Sent Events передают последовательность событий поверх долгого HTTP-соединения. Клиент читает их до одного из исходов:
created -> receiving -> completed
\-> failed
\-> disconnected
completed — терминальное событие протокола. failed — ошибка, переданная внутри stream. disconnected означает, что транспорт закончился без подтверждённого финала. Последний случай требует диагностики.
В OpenAI Responses API события имеют типы вроде response.created, фрагментов output и response.completed; также возможны failure или incomplete states. Anthropic Messages использует message_start, события content block, message_delta и message_stop. Названия нельзя смешивать в одном parser.
Хотите воспроизвести обрыв Streaming на контролируемом запросе? Можно создать собственный аккаунт BetterToken и API Key, открыть API reference и начать с короткого stream без tool calls. Затем сопоставьте время, модель и status с записью в Dashboard; только после проверки terminal event и частичного вывода добавляйте ограниченный retry.
Какие данные сохранить при обрыве
Минимальный журнал помогает отличить проблему клиента от сервера:
{
"started_at": "2026-08-03T12:00:00Z",
"protocol": "openai-responses",
"request_id": "req_placeholder",
"http_status": 200,
"last_event_type": "response.output_text.delta",
"events_received": 42,
"bytes_received": 8192,
"terminal_event_received": false,
"client_error": "socket closed"
}
Не сохраняйте API Key, полный prompt, tool arguments или чувствительный ответ. Частичный текст храните только там, где это допустимо по политике приложения. Для production полезнее записывать hash операции, число событий и последний безопасный sequence marker.
Request ID может прийти в HTTP headers или событии. Сохраните его как можно раньше, а не после завершения stream.
Проверьте parser до сети
Клиент должен корректно обрабатывать:
- несколько строк
data:в одном событии; - пустые строки между событиями;
- фрагменты UTF-8 на границе сетевых chunks;
- неизвестные типы событий без падения процесса;
- error event после успешного HTTP status;
- terminal event без обязательного последнего текстового delta;
- tool arguments, разбитые на несколько fragments.
TCP chunk не равен SSE event. Один event может прийти частями, а несколько events — одним чтением. Сначала соберите полноценный SSE frame, затем разбирайте JSON.
Псевдокод обработчика:
state = receiving
for event in parse_sse(response_body):
log_safe_metadata(event)
apply_event_to_partial_result(event)
if is_terminal_success(event):
state = completed
break
if is_terminal_failure(event):
state = failed
break
if connection_closed and state == receiving:
state = disconnected
Функции is_terminal_success и is_terminal_failure должны быть отдельными для Responses, Chat Completions и Messages.
Проверьте timeout на каждом слое
Долгое соединение проходит через несколько таймеров:
- timeout SDK или HTTP-клиента;
- idle/read timeout приложения;
- reverse proxy;
- load balancer или ingress;
- корпоративный proxy;
- мобильная или домашняя сеть;
- server-side generation limit.
Общий request timeout и idle timeout — разные параметры. Если модель регулярно присылает события, короткий idle timeout не должен срабатывать. Если между событиями допустимы длинные паузы, значение нужно согласовать с ожидаемой нагрузкой.
Проверьте buffering proxy. Когда proxy копит SSE chunks, пользователь долго не видит текст, а затем получает большой блок или timeout. Тестируйте время первого события и интервалы между событиями на каждом окружении: локально, за reverse proxy и в production.
Частичный вывод: показывать, сохранять или отбрасывать
Частичный текст полезен для интерфейса, но его статус должен быть явным. Не показывайте оборванный ответ как завершённый.
Удобная модель состояния:
streaming— текст ещё меняется;complete— получено terminal success event;partial— соединение оборвалось после нескольких events;failed— протокол передал ошибку;cancelled— запрос остановил пользователь или приложение.
Для partial сохраните уже полученный текст отдельно от нового retry. Склеивать два поколения автоматически опасно: модель может повторить часть ответа, изменить формулировку или вызвать tools в другом порядке.
Когда запрос можно повторить
Безопасность retry зависит от действия.
Текст без внешних действий
Короткий текстовый запрос обычно можно повторить с ограничением попыток. Приложение показывает старый результат как partial и новый — как отдельную генерацию либо заменяет его после явного подтверждения.
Tool calls и транзакции
Перед повтором проверьте, был ли tool уже выполнен. Если stream оборвался после отправки команды, второй запрос может повторно создать issue, письмо или платёжную операцию. Используйте idempotency key на уровне инструмента, собственный operation ID и журнал выполненных действий.
Длинная agent-задача
Автоматическое «продолжить с последнего токена» редко подтверждено протоколом. Лучше восстановить задачу из сохранённого application state: подтверждённых messages, результатов tools и последнего завершённого шага. Не выдавайте сырые текстовые fragments за консистентное состояние agent.
Ограниченный retry с backoff
Retry-политика должна иметь конечное число попыток:
attempts = 0
while attempts < MAX_ATTEMPTS:
result = run_request(operation_id)
if result.completed:
return result
if not result.retryable:
raise result.error
wait(base_delay * 2**attempts + random_jitter)
attempts += 1
Retryable определяется по protocol error, HTTP status, наличию terminal event и побочным эффектам. 401, неверный Model ID и невалидный JSON не исправятся от паузы. 429, временный 5xx или транспортный сбой иногда допускают повтор, но только с лимитом и учётом заголовков провайдера.
Минимальный тест
- Отправьте короткий streaming-запрос без tools.
- Запишите все event types и дождитесь terminal event.
- Искусственно оборвите клиент после нескольких events.
- Убедитесь, что результат помечен
partial. - Проверьте один ограниченный retry.
- Повторите за production proxy.
- Сопоставьте оба запроса по времени, модели и status в Dashboard.
Точные event types берите из официальной документации: OpenAI streaming Responses и Anthropic Messages streaming.
FAQ
Можно ли продолжить stream с последнего токена?
Универсального механизма для этого нет. Сохраните частичный результат и application state, затем следуйте возможностям конкретного API. Новый запрос может повторить или изменить текст.
Почему HTTP status 200, а stream закончился ошибкой?
Headers приходят до полной генерации. Ошибка может появиться позже как protocol event или транспортный обрыв. Поэтому одного status недостаточно.
Нужно ли повторять запрос после любого disconnect?
Нет. Сначала проверьте terminal event, error, request ID и побочные действия. Retry для tool call требует идемпотентности.
Где искать причину, если локально всё работает?
Проверьте reverse proxy, load balancer, idle timeout, buffering и корпоративную сеть. Сравните интервалы событий до и после каждого слоя.
Что сверить в BetterToken?
Откройте Dashboard и сопоставьте время, модель, status и usage. Актуальные параметры запроса сверяйте с API reference; не отправляйте в поддержку полный API Key или чувствительный prompt.