Назад в блог

API и SDKИнструкция

Стриминг ответов в Node.js через OpenAI SDK

Как получать ответ чата по токенам в Node.js: цикл for await, остановка по запросу, обработка ошибок и usage в OpenAI SDK.

Автор: CheapTokens TeamОпубликовано Обновлено 3 мин чтения

Окно терминала, в котором скрипт Node.js выводит потоковый ответ по токенам
Содержание
  1. Что понадобится
  2. Шаг 1. Стримим ответ через for await
  3. Шаг 2. Останавливаем поток по запросу
  4. Шаг 3. Обрабатываем ошибки
  5. Шаг 4. Читаем usage и оцениваем стоимость
  6. Полный скрипт
  7. Что дальше

Стриминг позволяет показывать ответ по мере генерации, а не ждать весь текст. В этом руководстве вы напишете небольшой скрипт на Node.js: он получает ответ чата от API CheapTokens по токенам, останавливается по запросу и обрабатывает ошибки. Версия для Python — в статье Стриминг ответов в Python.

Что понадобится#

  • Node.js 22 или новее (минимальная версия из README пакета openai, проверено 22 октября 2026 года).
  • API-ключ CheapTokens. Если его ещё нет, пройдите Первый запрос к API CheapTokens.
  • Официальный SDK: npm install openai.

У CheapTokens OpenAI-совместимый API по адресу https://api.cheaptokens.dev/v1, поэтому официальный пакет openai работает после замены только базового URL и ключа. Храните ключ в переменной окружения, а не в коде:

bash
export CHEAPTOKENS_API_KEY="your-key"

Шаг 1. Стримим ответ через for await#

Создайте файл stream.mjs (расширение .mjs позволяет использовать import и await на верхнем уровне):

javascript
import OpenAI from 'openai';

const client = new OpenAI({
  apiKey: process.env.CHEAPTOKENS_API_KEY,
  baseURL: 'https://api.cheaptokens.dev/v1',
});

const stream = await client.chat.completions.create({
  model: 'deepseek-ai/DeepSeek-V4-Flash-0731', // take the id from the CheapTokens docs
  messages: [{ role: 'user', content: 'Explain streaming in two sentences.' }],
  stream: true,
  stream_options: { include_usage: true },
});

let usage = null;
for await (const chunk of stream) {
  const delta = chunk.choices[0]?.delta?.content;
  if (delta) process.stdout.write(delta);
  if (chunk.usage) usage = chunk.usage;
}

console.log('\n\nusage:', usage);

Запустите: node stream.mjs. SDK читает Server-Sent Events от API и отдаёт по одному фрагменту (chunk) на каждую итерацию for await. Важны три детали:

  • Текст приходит в chunk.choices[0].delta.content. В некоторых фрагментах текста нет (первый часто содержит только роль), поэтому проверяйте, что delta не пуст.
  • process.stdout.write печатает без перевода строки, и слова появляются одно за другим.
  • С stream_options: { include_usage: true } последний фрагмент содержит объект usage с числом токенов запроса и ответа, а массив choices в нём пуст. Поэтому в коде chunk.choices[0]?. с опциональной цепочкой.

Id модели берите из документации CheapTokens: у разных провайдеров одна и та же модель называется по-разному.

Шаг 2. Останавливаем поток по запросу#

Пользователь нажимает «Стоп», закрывает вкладку или переключает чат. Отмените запрос, чтобы не ждать токены, которые никто не прочитает. Передайте AbortSignal в параметрах запроса:

javascript
const controller = new AbortController();

process.on('SIGINT', () => controller.abort()); // Ctrl+C stops the stream

const stream = await client.chat.completions.create(
  {
    model: 'deepseek-ai/DeepSeek-V4-Flash-0731',
    messages: [{ role: 'user', content: 'Write a long story about a lighthouse.' }],
    stream: true,
  },
  { signal: controller.signal },
);

У объекта потока есть и собственный stream.controller.abort(), а выход из цикла for await через break тоже закрывает соединение. Выбирайте удобное: сигнал — когда кнопка отмены в другом месте кода, break — когда решение принимается внутри цикла (например, встретилось стоп-слово).

Шаг 3. Обрабатываем ошибки#

Поток может упасть до первого токена (неверный ключ, пустой баланс) или посередине (обрыв сети). Оберните в один try и запрос, и цикл:

javascript
try {
  for await (const chunk of stream) {
    const delta = chunk.choices[0]?.delta?.content;
    if (delta) process.stdout.write(delta);
  }
} catch (err) {
  if (err instanceof OpenAI.APIUserAbortError) {
    console.log('\n[stopped by user]');
  } else if (err instanceof OpenAI.APIError) {
    console.error(`\nAPI error ${err.status}: ${err.message}`);
  } else {
    throw err;
  }
}
  • APIUserAbortError приходит после вашего abort(). Это не сбой, не показывайте его как ошибку.
  • У APIError есть HTTP-код status: 401 — неверный или отсутствующий ключ, 429 — слишком много запросов, сообщение про токены — балансу нужно пополнение. Другие причины и решения — в статьях Частые ошибки подключения и как их исправить и Ошибка «Not enough tokens».
  • Уже напечатанный текст после ошибки остаётся корректным. Сохраните его, если хотите показать частичный ответ.

Шаг 4. Читаем usage и оцениваем стоимость#

Объект usage содержит prompt_tokens и completion_tokens. Вместе с ценой модели за 1M токенов он даёт точную стоимость запроса. Для прикидки заранее используйте живую таблицу ниже: в ней стоимость 1000 запросов по 2000 входных и 800 выходных токенов.

Модель За запрос 1 000 запросов OpenRouter, 1 000 запросов
GLM-5.3-Flash $0.000072 $0.072 $0.143
DeepSeek-V4-Flash $0.00011 $0.11 $0.22
MiniMax-M2.7 $0.00022 $0.22 $1.56

2 000 входных и 800 выходных токенов на запрос. Цены за 1M токенов актуальны и меняются вместе с нашими тарифами; столбец OpenRouter показывает справочные цены для той же нагрузки.

Для потоковых ответов в чате MiniMax-M2.7 стоит $0.05 за 1M входных и $0.15 за 1M выходных токенов, контекст 180K, до 8 192 токенов в ответе. Цена модели фиксирована в любое время суток, один баланс работает для всех моделей, поэтому для сравнения на ваших промптах достаточно поменять строку model.

Полный скрипт#

javascript
import OpenAI from 'openai';

const client = new OpenAI({
  apiKey: process.env.CHEAPTOKENS_API_KEY,
  baseURL: 'https://api.cheaptokens.dev/v1',
});

const controller = new AbortController();
process.on('SIGINT', () => controller.abort());

try {
  const stream = await client.chat.completions.create(
    {
      model: 'deepseek-ai/DeepSeek-V4-Flash-0731',
      messages: [{ role: 'user', content: 'Explain streaming in two sentences.' }],
      stream: true,
      stream_options: { include_usage: true },
    },
    { signal: controller.signal },
  );

  let usage = null;
  for await (const chunk of stream) {
    const delta = chunk.choices[0]?.delta?.content;
    if (delta) process.stdout.write(delta);
    if (chunk.usage) usage = chunk.usage;
  }
  console.log('\n\nusage:', usage);
} catch (err) {
  if (err instanceof OpenAI.APIUserAbortError) {
    console.log('\n[stopped by user]');
  } else if (err instanceof OpenAI.APIError) {
    console.error(`\nAPI error ${err.status}: ${err.message}`);
  } else {
    throw err;
  }
}

Что дальше#

  • Отдавайте фрагменты в браузер через Server-Sent Events со своего эндпоинта, чтобы ключ не покидал сервер.
  • Пробуйте другие модели, меняя только строку model: MiniMax-M2.7 доступна с тем же балансом и тем же базовым URL.
  • Получите ключ и запустите скрипт: создайте API-ключ CheapTokens и пополните баланс.

Адреса API, id моделей и лимиты — в документации CheapTokens.