1. Запрос
Streaming доступен через Responses API у моделей, которые поддерживают этот маршрут. Минимальный запрос — curl ниже: обратите внимание на stream: true и ограничение max_output_tokens, без которого поток может идти неожиданно долго и дорого.
curl https://api.normahub.cc/v1/responses \
-H "Authorization: Bearer $NORMAHUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "MODEL_ID",
"input": "Объясни результат по шагам",
"max_output_tokens": 512,
"stream": true
}'Ответ передаётся как text/event-stream. Читайте события последовательно и извлекайте содержимое из строк data:.
2. Чтение потока байтов
Главная ошибка новичков — считать сетевой chunk событием. Один chunk может содержать несколько SSE-событий или половину строки: клиент обязан накапливать буфер, декодировать UTF-8 потоково и резать события по пустой строке, сохраняя хвост до следующего чтения. Data-события разбирайте отдельно от служебных маркеров, завершение — отдельным состоянием.
const response = await fetch("https://api.normahub.cc/v1/responses", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.NORMAHUB_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ model: "MODEL_ID", input: "Привет", stream: true }),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let done = false;
while (!done) {
const { value, done: finished } = await reader.read();
done = finished;
buffer += decoder.decode(value ?? new Uint8Array(), { stream: !done });
const parts = buffer.split("
");
buffer = parts.pop() ?? "";
for (const event of parts) {
if (!event.startsWith("data:")) continue; // служебные маркеры отдельно
const payload = event.slice(5).trim();
if (payload === "[DONE]") { done = true; break; }
process.stdout.write(payload);
}
}
reader.releaseLock();При отмене пользователем закрывайте reader явно и фиксируйте состояние «отменён пользователем», а не «ошибка»: это разные ветки аналитики и биллинга.
3. Обрыв — не завершение
Не считайте разрыв TCP успешным завершением. Дождитесь финального события потока, закройте reader и сохраните X-Request-Id. Gateway завершает запись usage и стоимости при закрытии streaming-операции, поэтому итог запроса существует даже если пользователь закрыл вкладку на середине.
Минимальный набор тестов перед продом: короткий ответ, длинный ответ, медленный ответ, отмена пользователем, обрыв сети, ошибка после первого события. Для каждого зафиксируйте, что видит пользователь и какой итог сохраняется в usage. Повтор после частично полученного ответа — только с явной стратегией защиты от дублирования.
4. Ограничения и таймауты
В текущей версии NormaHub streaming не включён для /v1/chat/completions и /v1/messages. Для них отправляйте stream: false либо используйте Responses API.
Задавайте отдельные таймауты подключения и чтения. После частично полученного ответа не выполняйте автоматический повтор без явной стратегии защиты от дублирования. Ошибки, пришедшие событием после начала потока, разбирайте по гайду по ошибкам, а цену потока — по гайду по стоимости. Архитектура долгих потоковых задач — в статье про long-running агентов.