Стриминг
AITUNNEL отдаёт стрим от любой модели. Интерфейс обновляется по мере генерации, не дожидаясь полного ответа.
Передайте stream: true. Ответ приходит чанками в формате Server-Sent Events: строки data: {…}, в /chat/completions поток заканчивается строкой data: [DONE].
Работает в /chat/completions, /responses и /messages. Голосовой ответ модели стримится в delta.audio — аудио.
Как включить
Проще всего читать поток через OpenAI SDK. Сырой HTTP тоже подходит — парсите строки data:.
curl https://api.aitunnel.ru/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-aitunnel-xxx" \
-N \
-d '{
"model": "gpt-5.6-sol",
"stream": true,
"messages": [
{ "role": "user", "content": "Как построить самое высокое здание в мире?" }
]
}'В финальном чанке (часто с пустым choices) приходит usage — токены, cost, cost_rub и balance. Подробнее о схеме — справочник API.
Три API — три формата потока
/chat/completions | /responses | /messages | |
|---|---|---|---|
Строка event: | нет, только data: | есть, совпадает с type в JSON | есть, совпадает с type в JSON |
| Чем заканчивается | data: [DONE] | событие response.completed (или response.incomplete, error) | событие message_stop (или error) |
data: [DONE] | всегда | может прийти после последнего события, может не прийти — не ждите его | не приходит |
Где usage | в последнем чанке | в response.usage события response.completed / response.incomplete | в usage события message_delta |
| ID запроса | id каждого чанка, chat-… | response.id, resp-… | message.id в message_start, msg-… |
ID запроса в потоке тот же, что в заголовке X-Request-Id ответа.
Разбор SSE вручную
Если SDK нет, читайте тело ответа построчно. Каждое событие — строка data:, затем JSON. В /chat/completions строка data: [DONE] — конец потока.
Пока модель читает длинный контекст или думает, данных может не быть минутами. Чтобы прокси и сетевые фильтры не закрыли простаивающее соединение, раз в 10 секунд приходит SSE-комментарий : AITUNNEL PROCESSING — во всех трёх API. По спецификации SSE строки с : — комментарии: SDK их пропускают сами, а в ручном разборе пропускайте всё, что не начинается с data: (в /responses и /messages — ещё и event: ). Не передавайте такую строку в JSON.parse.
const response = await fetch('https://api.aitunnel.ru/v1/chat/completions', {
method: 'POST',
headers: {
Authorization: 'Bearer sk-aitunnel-xxx',
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'gpt-5.6-sol',
stream: true,
messages: [{ role: 'user', content: 'Как построить самое высокое здание в мире?' }],
}),
});
if (!response.ok) {
const { error } = await response.json();
throw new Error(`${error.type}: ${error.message} (${error.request_id})`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
while (true) {
const lineEnd = buffer.indexOf('\n');
if (lineEnd === -1) break;
const line = buffer.slice(0, lineEnd).trim();
buffer = buffer.slice(lineEnd + 1);
if (!line.startsWith('data: ')) continue;
const data = line.slice(6);
if (data === '[DONE]') return;
const parsed = JSON.parse(data);
if (parsed.error) {
// Сбой после начала генерации: HTTP уже 200, причина — в событии.
throw new Error(`${parsed.error.type}: ${parsed.error.message} (${parsed.error.request_id})`);
}
if (parsed.usage) {
console.log('Usage:', parsed.usage);
}
const content = parsed.choices?.[0]?.delta?.content;
if (content) process.stdout.write(content);
}
}Рекомендуемые клиенты: OpenAI SDK и Vercel AI SDK. Они сами собирают чанки.
Остановка потока
Закройте соединение (AbortController, stream.close(), Ctrl+C в curl) — мы сразу отменяем генерацию у провайдера.
- Списывается то, что провайдер успел обработать до остановки: вход запроса и уже сгенерированные токены. Остановленный поток не бесплатен.
- Точную сумму мы запрашиваем у провайдера уже после обрыва, поэтому запрос появляется в истории расходов и в GET /aitunnel/requests/{id} не сразу — обычно в течение минуты.
- Если провайдер так и не сообщил стоимость, списывается оценка по тому, что успело прийти в потоке.
Если денег хватает не на весь ответ
Когда баланса или бюджета ключа не хватает на полный max_tokens (или на максимум модели, если лимит не задан), запрос не отклоняется — вывод ограничивается тем, на что хватает денег. В ответе приходит заголовок X-AITunnel-Output-Capped с этим числом токенов. Подробнее — лимит расходов.
Если модель дошла до этого предела или деньги закончились по ходу потока (их потратили параллельные запросы), поток завершается как при обычном лимите токенов:
# Chat Completions
data: {"id":"chat-034pVqy5IBvKq1Ef5KcGE5","object":"chat.completion.chunk","created":1234567890,"model":"gpt-5.6-sol","choices":[{"index":0,"delta":{},"finish_reason":"length"}]}
data: [DONE]
# Responses
event: response.incomplete
data: {"type":"response.incomplete","response":{"id":"resp-034pVqy5IBvKq1Ef5KcGE5","object":"response","created_at":1234567890,"status":"incomplete","model":"gpt-5.6-sol","incomplete_details":{"reason":"max_output_tokens"},"error":null,"output":[],"usage":null}}
# Messages
event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"max_tokens","stop_sequence":null},"usage":{"output_tokens":0}}
event: message_stop
data: {"type":"message_stop"}То есть finish_reason: "length" в Chat Completions, response.incomplete с incomplete_details.reason: "max_output_tokens" в Responses и stop_reason: "max_tokens" в Messages. Когда поток остановили мы, а не провайдер, события с usage в нём нет: стоимость считается после, как при остановке потока.
Ошибки в стриме
Поведение зависит от того, успели ли уйти токены.
До первого токена
Обычный JSON с HTTP-статусом ошибки:
{
"error": {
"code": 400,
"type": "model_not_found",
"message": "Указанная модель (gpt-5.6-sool) не найдена, либо сейчас недоступна. Список доступных моделей на нашем сайте https://aitunnel.ru/models",
"request_id": "chat-034pVqy5IBvKq1Ef5KcGE5"
}
}- 400 — неверные параметры
- 401 — ключ
- 402 — не хватает баланса
- 429 — лимит запросов
- 502 — провайдер не ответил
Полный список — ошибки и отладка.
Сбой провайдера в первые секунды, до первого токена, мы обрабатываем сами: повторяем запрос или переключаемся на запасную модель. В поток такая ошибка не попадает.
После начала генерации
HTTP уже 200, ошибка приходит событием SSE. В нём тот же объект error, что в обычной ошибке: code, type, message, request_id. После этого события поток заканчивается.
Chat Completions — последний чанк с finish_reason: "error" и полем error, за ним data: [DONE]:
data: {"id":"chat-034pVqy5IBvKq1Ef5KcGE5","object":"chat.completion.chunk","created":1234567890,"model":"gpt-5.6-sol","choices":[{"index":0,"delta":{},"finish_reason":"error"}],"error":{"code":502,"type":"provider_unavailable","message":"Соединение с провайдером модели прервалось. Повторите запрос.","request_id":"chat-034pVqy5IBvKq1Ef5KcGE5"}}
data: [DONE]Responses — событие error. Поля code и message на верхнем уровне — как в событии error OpenAI, наш объект — в error. После него ничего не приходит, response.completed не будет:
event: error
data: {"type":"error","code":"provider_unavailable","message":"Соединение с провайдером модели прервалось. Повторите запрос.","param":null,"error":{"code":502,"type":"provider_unavailable","message":"Соединение с провайдером модели прервалось. Повторите запрос.","request_id":"resp-034pVqy5IBvKq1Ef5KcGE5"}}Messages — событие error в формате Anthropic: error.type из словаря Anthropic, наш тип в error.aitunnel_type, request_id на верхнем уровне. message_stop после него не приходит:
event: error
data: {"type":"error","error":{"type":"overloaded_error","message":"Overloaded","code":503,"aitunnel_type":"provider_unavailable"},"request_id":"msg-034pVqy5IBvKq1Ef5KcGE5"}То, что модель успела сгенерировать до сбоя, провайдер обычно тарифицирует — такой запрос списывается по факту, как остановленный поток.
from openai import OpenAI, APIError, APIStatusError
client = OpenAI(
api_key="sk-aitunnel-xxx",
base_url="https://api.aitunnel.ru/v1/",
)
try:
stream = client.chat.completions.create(
model="gpt-5.6-sol",
messages=[{"role": "user", "content": "Как построить самое высокое здание в мире?"}],
stream=True,
)
for chunk in stream:
delta = chunk.choices[0].delta.content if chunk.choices else None
if delta:
print(delta, end="", flush=True)
except APIStatusError as e:
# До потока: обычный HTTP-ответ с ошибкой
err = e.response.json().get("error") or {}
print("Запрос отклонён:", e.status_code, err.get("type"), err.get("message"))
print("ID запроса:", err.get("request_id"))
except APIError as e:
# После начала генерации: SDK сам поднимает исключение на событии с error
print("Поток оборвался:", e.message)
print("Детали:", e.body) # {"code", "type", "message", "request_id"}