Ответ модели можно дождаться целиком, а можно читать по мере генерации. Второй путь называется стримингом через server-sent events (SSE). Ниже разбираем, где он окупается, а где только добавляет код.
Как устроен стриминг
В обычном запросе сервер держит соединение, пока не допишет весь ответ, и отдаёт его одним куском. В стриминге он шлёт события, как только готовы очередные токены. Клиент открывает HTTP-соединение с заголовком Accept: text/event-stream и читает поток строк data: {...}, между которыми стоит пустая строка.
У Anthropic и OpenAI форматы разные. Anthropic присылает именованные события (message_start, content_block_delta, message_stop и другие), и у каждого есть своё поле type. Поток OpenAI проще: те же строки data: с дельтой текста и финальный data: [DONE]. В codexkey работают оба формата, а стриминг включается тем же параметром stream: true, что и в оригинальных SDK.
Где стриминг даёт реальный выигрыш
Чат. Между «пять секунд крутится спиннер» и «текст пошёл через триста миллисекунд» лежит заметная разница, хотя общее время может совпадать. Человек начинает читать раньше, и сервис кажется отзывчивым.
Длинные ответы. Когда модель пишет пару тысяч токенов, читатель успевает пройти начало, пока дописывается конец. Неподвижный экран всё это время раздражает.
Отмена. Пользователь видит, что ответ уходит не туда, жмёт стоп, соединение закрывается, и за токены, которых так и не дождались, платить не нужно.
Когда стриминг не нужен
Короткий ответ: классификация, одно извлечённое поле, «да» или «нет». Такое по частям никто не читает, а код от стриминга становится сложнее.
Серверная обработка без интерфейса, где результат уходит в базу или другую систему. Там нужен полный, проверенный ответ. Частичный текст всё равно пришлось бы копить целиком, и смысл стриминга пропадает.
Батч из десятков или сотен запросов тоже проще без него: меньше состояния, проще повторы, проще учёт токенов.
Как собирать частичные чанки
Главная ловушка: сетевой чанк не совпадает со смысловой единицей текста. Он может оборвать слово или JSON на полпути, особенно если заодно стримится tool use с аргументами функции, пришедшими лишь частично.
Что работает: копить текстовые дельты в буфере и не трогать JSON, пока не придёт событие о закрытии блока. Пример на Python с SDK Anthropic:
import anthropic
client = anthropic.Anthropic(
api_key="ваш-ключ-codexkey",
base_url="https://api.codexkey.ru/v1",
)
with client.messages.stream(
model="claude-sonnet-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Напиши короткое стихотворение про осень"}],
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
final = stream.get_final_message()
SDK сам склеивает дельты в цельный текст, а в конце отдаёт готовое сообщение с точным числом токенов. Если вы читаете сырой SSE без SDK, повторите эту логику вручную: складывайте строки в буфер и разбирайте JSON только на границе события.
Что делать при разрыве соединения
Длинный стрим может оборваться: приложение ушло в фон, мобильная сеть моргнула, прокси закрыл соединение по таймауту. Штатного способа продолжить с места обрыва в LLM API нет, в отличие от загрузки файлов с докачкой.
Практика простая. Считайте обрыв обычной сетевой ошибкой, сохраните то, что уже пришло, и отдельно решите судьбу этого текста: показать с пометкой «прервано» или отправить запрос заново. При повторе имеет смысл ужать max_tokens до реально нужного остатка, чтобы не заплатить дважды без причины.
Когда обрывы случаются именно на длинных генерациях, виноват нередко не клиент, а промежуточный прокси или балансировщик с коротким таймаутом. Увеличьте таймаут на клиенте и проверьте, нет ли между приложением и codexkey посредника, который сам режет долгие соединения.
Итог
SSE стоит включать там, где человек ждёт и читает: в чате, на длинных ответах, при возможности остановить генерацию. В серверной обработке без интерфейса он лишь добавляет сложности. Копите дельты в буфере, разбирайте JSON на границе события, а обрыв встречайте как сетевую ошибку, не потеряв уже полученный текст.