Руководства
Server-Sent Events Streaming
ZvenoAI полностью поддерживает потоковую передачу через Server-Sent Events (SSE), совместимую с форматом OpenAI Chat Completions. Streaming обеспечивает прогрессивный рендеринг ответов, снижая воспринимаемую задержку и позволяя отменять запросы.
Обзор
Потоковая передача (streaming) позволяет получать ответы LLM по мере их генерации, токен за токеном, вместо ожидания полного ответа. Это критично для создания отзывчивых интерфейсов и улучшения пользовательского опыта.
Ключевые преимущества:
- • Снижение воспринимаемой задержки — пользователь видит прогресс сразу
- • Возможность прервать генерацию в любой момент
- • Лучший UX для длинных ответов (статьи, код, аналитика)
- • Совместимость с OpenAI SDK и протоколом
Протокол SSE
При установке stream: true в запросе POST /v1/chat/completions, ZvenoAI возвращает ответ в формате Server-Sent Events:
Формат события:
data: {"id":"chatcmpl-...","choices":[{"delta":{"content":"токен"}}],...}
: OPENROUTER PROCESSING
data: [DONE]Типы событий
1. Incremental Deltas
Основной тип событий — частичные данные, которые приходят по мере генерации:
choices[0].delta.content— текстовые токены (могут приходить частями)choices[0].delta.tool_calls— вызовы инструментов (если модель использует tools)choices[0].delta.reasoning_details— блоки рассуждений (для reasoning-моделей)
Обработка deltas:
- • Добавляйте
delta.contentв буфер текста - • Объединяйте
delta.tool_callsпоid - • Сохраняйте
delta.reasoning_detailsв исходном порядке
2. Keep-Alive Комментарии
ZvenoAI отправляет комментарии для поддержания соединения:
: OPENROUTER PROCESSING
По спецификации SSE строки, начинающиеся с :, являются комментариями и должны игнорироваться клиентом. Большинство SDK делают это автоматически.
3. Обработка ошибок
ZvenoAI различает два сценария ошибок:
Ошибка до первого токена:
Возвращается стандартный HTTP-ответ с соответствующим статусом (400/401/402/429/5xx) и JSON-телом с описанием ошибки.
Mid-stream ошибка:
HTTP-статус уже 200 (поток начался), поэтому ошибка приходит как SSE-событие с полем error на верхнем уровне и finish_reason: "error".
data: {"error":{"message":"..."},"choices":[{"finish_reason":"error"}]}Рекомендация: Проверяйте каждое data:-событие на наличие поля error. При обнаружении — прекращайте чтение и отображайте ошибку пользователю.
4. Финальное событие (перед [DONE])
Перед отправкой data: [DONE], ZvenoAI может включить финальное SSE-событие с finish_reason: "stop" и полным объектом message:
Зачем это нужно:
- •
message.reasoning_details— полные блоки рассуждений - •
message.tool_calls— завершенные вызовы инструментов - •
message.images— изображения (multimodal) - •
message.content— структурированный контент (массив частей)
При сохранении в историю диалога используйте это финальное message, а не только склеенный текст из delta.content.
Сохранение Reasoning Blocks
Для reasoning-моделей (например, o1, o3) при использовании tool calling провайдер может требовать, чтобы при следующем запросе вы вернули обратно в messages[]:
- • Assistant-сообщение с
tool_calls - • Все
reasoning_detailsв точной последовательности - • Связанные content-блоки, как они были получены
Важно:
Эти блоки нельзя переставлять или редактировать. Для Gemini некоторые tool/function-блоки требуют сохранения thought_signature. ZvenoAI поддерживает round-trip: если клиент вернул такие поля обратно в messages, они будут проброшены в OpenRouter без потерь.
Алгоритм обработки
- Установите
stream: trueв запросе completion - Читайте SSE-события:
- • Игнорируйте строки, начинающиеся с
:(keep-alive) - • Парсите только события
data: ...
- • Игнорируйте строки, начинающиеся с
- Если
data == "[DONE]"— завершайте чтение - Если JSON содержит поле
error— завершайте и показывайте ошибку пользователю - Иначе:
- • Добавляйте
choices[0].delta.contentв UI - • Аккумулируйте
tool_calls/reasoning_details - • Если пришёл финальный
message— используйте его для сохранения в историю
- • Добавляйте
Примеры кода
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.ZVENOAI_API_KEY,
baseURL: "https://api.zveno.ai/v1",
});
const stream = await client.chat.completions.create({
model: "openai/gpt-4o",
messages: [{ role: "user", content: "Придумай романтичный тост." }],
stream: true,
});
let fullText = "";
for await (const evt of stream) {
// SDK автоматически парсит data events и отфильтровывает keep-alive комментарии
// Проверка на mid-stream ошибку
if (evt.error) {
throw new Error(evt.error.message || "Stream error");
}
const choice = evt.choices?.[0];
const delta = choice?.delta;
// Обработка текстовых токенов
const content = delta?.content;
if (typeof content === "string" && content.length) {
fullText += content;
process.stdout.write(content);
}
// Финальное сообщение с полными данными
const msg = choice?.message;
if (msg) {
// msg содержит: reasoning_details, tool_calls, images, content parts
// Используйте msg для сохранения в историю диалога
console.log("\nФинальное сообщение:", msg);
}
}Рекомендации по UX
Индикация прогресса
Показывайте индикатор «Запрос обрабатывается» до получения первого токена или первого keep-alive комментария (подтверждает активное соединение).
Возможность отмены
Добавьте кнопку остановки генерации с использованием AbortController (JS) или context.CancelFunc (Go).
Метрики производительности
Отслеживайте TTFT (Time To First Token) — критичная метрика для пользовательского опыта. Логируйте её для мониторинга производительности.
Буферизация и сохранение
Всегда собирайте ответ в буфер для:
- • Отображения частичного результата при ошибке
- • Корректного сохранения в историю диалога
- • Возможности повторной обработки данных
Предпочтительно сохранять финальный message перед [DONE], а не только конкатенированный текст.
Tool Calling + Reasoning
При использовании инструментов с reasoning-моделями сохраняйте и возвращайте reasoning_details и tool blocks без изменений, включая thought_signature (если присутствует).