Потоковый ответ

Как получать ответ модели по частям, из каких кадров он состоит, где искать расход и что происходит при обрыве или ошибке посреди потока.

Обновлено: 3 мин чтения

Включите поток

Добавьте в запрос "stream": true, и ответ пойдёт по мере появления. Чтобы получить расход, добавьте и stream_options со значением include_usage.

import os
from openai import OpenAI

client = OpenAI(
    base_url="https://epicai.ru/v1",
    api_key=os.environ["EPICAI_API_KEY"],
)

stream = client.chat.completions.create(
    model="gpt-6-luna",
    messages=[{"role": "user", "content": "Напиши три слова: привет, мир, epicai."}],
    max_tokens=100,
    stream=True,
    stream_options={"include_usage": True},
)

for chunk in stream:
    if chunk.choices and chunk.choices[0].delta.content:
        print(chunk.choices[0].delta.content, end="", flush=True)
    if chunk.usage:
        print("\nСписано TOK:", chunk.model_extra["epic"]["cost_tok"])
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://epicai.ru/v1",
  apiKey: process.env.EPICAI_API_KEY,
});

const stream = await client.chat.completions.create({
  model: "gpt-6-luna",
  messages: [{ role: "user", content: "Напиши три слова: привет, мир, epicai." }],
  max_tokens: 100,
  stream: true,
  stream_options: { include_usage: true },
});

for await (const chunk of stream) {
  const piece = chunk.choices[0]?.delta?.content;
  if (piece) process.stdout.write(piece);
  if (chunk.usage) console.log("\nСписано TOK:", chunk.epic.cost_tok);
}

Без библиотеки поток читают построчно, у curl для этого есть ключ -N, он отключает буферизацию вывода.

Из чего состоит поток

Ответ приходит как server-sent events с типом text/event-stream. Каждое событие — строка data: с JSON и пустая строка после неё. Идентификатор id и время created одинаковы во всех кадрах, а в конце стоит служебная строка [DONE]. Это не JSON, её не разбирают.

data: {"id":"chatcmpl-7ca0…","object":"chat.completion.chunk","created":1793095684,"model":"gpt-6-luna","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}

data: {"id":"chatcmpl-7ca0…","object":"chat.completion.chunk","created":1793095684,"model":"gpt-6-luna","choices":[{"index":0,"delta":{"content":"пр"},"finish_reason":null}]}

data: {"id":"chatcmpl-7ca0…","object":"chat.completion.chunk","created":1793095684,"model":"gpt-6-luna","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}

data: {"id":"chatcmpl-7ca0…","object":"chat.completion.chunk","created":1793095684,"model":"gpt-6-luna","choices":[],"usage":{"prompt_tokens":16,"completion_tokens":11,"total_tokens":27},"epic":{"cost_tok":1,"request_id":"req_7ca0…"}}

data: [DONE]
  • Первый кадр несёт роль assistant и пустой content.
  • Дальше идут кадры с кусочками текста в delta.content. Рассуждения модели, если вендор их отдаёт, приходят в delta.reasoning_content, а вызовы функций в delta.tool_calls (формат разобран в статье Вызов функций).
  • Предпоследний обычный кадр содержит пустой delta и finish_reason.
  • Кадр с usage и epic приходит только при include_usage, и у него пустой массив choices. Код, который читает choices[0] без проверки, на этом кадре падает.

Если модель молчит долго, epicai раз в 10 секунд шлёт строку-комментарий : keep-alive, чтобы прокси не закрыл соединение. Клиенты на основе SSE её пропускают, а свой разбор должен пропускать строки, которые начинаются с двоеточия.

Где взять расход

Заголовок x-epic-cost-tok в потоковом ответе не приходит, потому что заголовки уходят раньше, чем известна сумма. Расход лежит в кадре с usage, в поле epic.cost_tok. Без include_usage числа в потоке нет, хотя TOK списываются как обычно. Сумму тогда покажут GET /v1/usage и история операций на сайте.

Если что-то пошло не так

Ошибка до первого байта приходит обычным ответом с кодом 4xx или 5xx и телом {"error": …}. Когда поток уже начался, код 200 отправлен, и вместо смены статуса epicai присылает кадр с ошибкой, а за ним [DONE].

data: {"error":{"message":"Модель не ответила вовремя","type":"server_error","param":null,"code":"upstream_timeout"}}

data: [DONE]

Поэтому разбор потока должен проверять наличие ключа error в каждом кадре. Всё, что модель успела отдать до сбоя, остаётся у вас и оплачивается, а если не отдано ничего, TOK не списываются.

Закрытие соединения на вашей стороне epicai замечает, прекращает приём ответа у вендора и списывает токены за доставленную часть. Если вендор успел сообщить расход, берётся он, иначе токены оцениваются по уже отправленному тексту. Максимальная длительность потока 900 секунд.

Не нашли ответ?

Посмотрите частые вопросы или напишите в поддержку.

Войти или создать аккаунт

20 000 токенов в подарок новым пользователям. Без карты и без VPN.

или через сервис

Продолжая, вы получаете сервисные письма.

Для защиты от роботов используется Yandex SmartCaptcha. Условия обработки данных.