Потоковые ответы
Как включить streaming в POST /v1/chat/completions и разбирать SSE-чанки в GonkaGate.
Добавьте stream: true в OpenAI-совместимый POST /v1/chat/completions, чтобы получать Server-Sent Events (SSE) из GonkaGate по мере генерации токенов. Читайте каждое data:-событие инкрементально, игнорируйте keep-alive-комментарии и считайте ответ завершённым только после финального чанка с usage и [DONE].
Сначала добейтесь рабочего непотокового запроса. Streaming нужен для чат-интерфейсов, длинных ответов и операторских сценариев, где важен частичный вывод. Если нужен только финальный результат, JSON-ответ проще выкатить и отладить.
Минимальный рабочий пример
export GONKAGATE_API_KEY="gp-your-api-key"
curl -N https://api.gonkagate.com/v1/chat/completions \
-H "Authorization: Bearer $GONKAGATE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "qwen/qwen3-235b-a22b-instruct-2507-fp8",
"messages": [
{ "role": "user", "content": "Объясни SSE одним предложением." }
],
"stream": true
}'Используйте -N, чтобы curl печатал поток по мере прихода чанков. Замените ID модели на модель, доступную вашему аккаунту. Для запроса также нужен валидный API-ключ и достаточный предоплаченный USD-баланс.
Как устроен SSE-поток
Успешный потоковый ответ приходит как последовательность SSE-событий, а не как один JSON-документ:
data: {"id":"chatcmpl-abc","object":"chat.completion.chunk","created":1735500000,"model":"qwen/qwen3-32b-fp8","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}
data: {"id":"chatcmpl-abc","object":"chat.completion.chunk","created":1735500000,"model":"qwen/qwen3-32b-fp8","choices":[{"index":0,"delta":{"content":"Hello"},"finish_reason":null}]}
data: {"id":"chatcmpl-abc","object":"chat.completion.chunk","created":1735500000,"model":"qwen/qwen3-32b-fp8","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: {"id":"chatcmpl-abc","object":"chat.completion.chunk","created":1735500000,"model":"qwen/qwen3-32b-fp8","choices":[],"usage":{"prompt_tokens":10,"completion_tokens":5,"total_tokens":15,"base_cost_usd":0.000075,"platform_fee_usd":0.0000075,"total_cost_usd":0.0000825}}
data: [DONE]Читать поток стоит в таком порядке:
- Первый чанк может только объявить роль
assistantи ещё не содержать текст. - Текст обычно приходит в следующих чанках через
choices[0].delta.content. - Последний чанк с текстом может содержать
finish_reason. - Финальные usage и cost приходят в отдельном чанке только с
usageиchoices: []. - Игнорируйте keep-alive-комментарии вроде
: keep-aliveи другие не-data:строки. - Поток завершён только после
[DONE].
Минимальный пример парсера
const response = await fetch("https://api.gonkagate.com/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.GONKAGATE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "qwen/qwen3-235b-a22b-instruct-2507-fp8",
messages: [{ role: "user", content: "Объясни SSE одним предложением." }],
stream: true,
}),
});
if (!response.ok) {
throw new Error(`Streaming request failed with ${response.status}`);
}
const reader = response.body?.getReader();
if (!reader) {
throw new Error("Response body is not readable");
}
const decoder = new TextDecoder();
let buffer = "";
let streamDone = false;
let finalUsage: Record<string, unknown> | null = null;
while (!streamDone) {
const { done, value } = await reader.read();
if (done) {
throw new Error("Stream ended before [DONE]");
}
buffer += decoder.decode(value, { stream: true });
while (true) {
const newlineIndex = buffer.indexOf("\n");
if (newlineIndex === -1) {
break;
}
const line = buffer.slice(0, newlineIndex).trim();
buffer = buffer.slice(newlineIndex + 1);
if (!line.startsWith("data:")) {
continue;
}
const payload = line.slice(5).trim();
if (payload === "[DONE]") {
streamDone = true;
break;
}
const chunk = JSON.parse(payload) as {
error?: { message?: string; type?: string; code?: string };
choices?: Array<{ delta?: { content?: string } }>;
usage?: Record<string, unknown>;
};
if (chunk.error) {
throw new Error(
`Streaming error (${chunk.error.code ?? chunk.error.type ?? "unknown"}): ${chunk.error.message ?? "Unknown error"}`
);
}
const content = chunk.choices?.[0]?.delta?.content;
if (content) {
process.stdout.write(content);
}
if (chunk.usage) {
finalUsage = chunk.usage;
}
}
}
console.log("\nFinal usage:", finalUsage);Этот паттерн оставляет парсер простым: игнорируйте не-data: строки, разбирайте каждый полный SSE payload, выводите delta.content по мере прихода, останавливайтесь на SSE-событии ошибки, считайте преждевременное закрытие потока ошибкой и считайте usage или cost финальными только после последнего чанка с usage.
Частые ошибки и сбои
- Клиент, который ждёт один полный JSON-ответ, будет выглядеть «зависшим», даже если стрим здоровый. Нужен клиент с поддержкой SSE или инкрементальное чтение ответа.
- Не считайте ответ завершённым по первому токену. Завершение наступает только после
[DONE]. - Не финализируйте стоимость или usage до последнего чанка с usage.
- Если ошибка случилась после старта стрима, не ждите обычный HTTP status
4xx/5xx. Обрабатывайтеdata: {"error": ...}как терминальное SSE-событие. - Не стройте парсер вокруг GonkaGate-specific prelude events для public
privacy-sanitization. Держите потоковый парсер сфокусированным на обычных SSEdata:payload'ах, keep-alive-комментариях и[DONE]. - Разрыв соединения в середине потока означает незавершённый ответ. Retry, fallback и пользовательскую реакцию держите отдельно от самого парсера.
См. также
- Создать chat completion, если нужны точные поля
POST /v1/chat/completions, SSE-схема ответа и контракт эндпоинта. - Быстрый старт, если у вас ещё нет первого успешного непотокового запроса.
- Обработка ошибок API для решений retry vs stop после сбоев стрима.