返回博客

API 与 SDK指南

使用 OpenAI SDK 在 Node.js 中流式传输聊天响应

在 Node.js 中逐 token 流式获取聊天补全:for await 循环、按需停止、错误处理以及使用 OpenAI SDK 的 token 用量。

作者:CheapTokens Team发布于 更新于 4 分钟阅读

终端窗口逐 token 打印来自 Node.js 脚本的流式答案
本页内容
  1. 你需要准备
  2. 第 1 步。使用 for await 流式获取答案
  3. 第 2 步。按需停止流
  4. 第 3 步。处理错误
  5. 第 4 步。读取用量并估算成本
  6. 完整脚本
  7. 后续步骤

流式传输让你的应用在答案生成时就能展示,而不必等待完整回复。在本指南中,你将构建一个小型 Node.js 脚本,从 CheapTokens API 逐 token 流式获取聊天补全,按需停止并处理错误。本主题的 Python 版本见 Python 中的流式响应。

你需要准备#

  • Node.js 22 或更新版本(openai 包 README 中声明的最低版本,检查于 2026 年 10 月 22 日)。
  • 一个 CheapTokens API 密钥。如果还没有,请按照 使用 CheapTokens 发出你的第一个 API 请求 操作。
  • 官方 SDK:npm install openai。

CheapTokens 在 https://api.cheaptokens.dev/v1 提供兼容 OpenAI 的 API,因此官方 openai 包只需更改 base 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 会读取来自 API 的 Server-Sent Events,并在 for await 的每次迭代中交给你一个数据块。有三个细节很重要:

  • 文本位于 chunk.choices[0].delta.content 中。有些数据块不携带文本(第一个通常只包含 role),因此要检查 delta 是否为空。
  • process.stdout.write 打印时不换行,因此单词会一个接一个出现。
  • 使用 stream_options: { include_usage: true } 时,最后一个数据块携带一个 usage 对象,包含提示和补全的 token 计数,其 choices 数组为空。这就是代码使用带可选链的 chunk.choices[0]?. 的原因。

模型 id 来自 CheapTokens 文档:不同提供商对同一模型的命名不同,因此请从那里复制 id。

第 2 步。按需停止流#

用户会按下停止、关闭标签页或切换聊天。取消请求,这样你就不必等待没人会读取的 token。在请求选项中传入一个 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(),用 break 离开 for await 循环也会关闭连接。选择离你代码更近的方式:当取消按钮位于别处时使用 signal,当决定在循环内部做出时(例如你发现了停止词)使用 break。

第 3 步。处理错误#

流可能在第一个 token 之前失败(密钥错误、余额为空),也可能在中间失败(网络中断)。将请求和循环都包在一个 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 表示请求过多,关于 token 的消息表示余额需要充值。更多原因和修复方法见 常见连接错误及如何修复 和 "Not enough tokens" 错误。
  • 已经打印的文本在出错后仍然有效。如果你想展示部分答案,可以保留它。

第 4 步。读取用量并估算成本#

usage 对象包含 prompt_tokens 和 completion_tokens。结合每 1M token 的模型价格,你就能得到一次请求的精确成本。若要在运行任何内容之前进行粗略估算,请使用下面的实时表格:它显示 1,000 次请求的成本,每次请求 2,000 个输入 token 和 800 个输出 token。

模型 每次请求 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 个输入 token 和 800 个输出 token。每 100 万 token 的价格为当前价格,会随我们的资费一同变动;OpenRouter 列显示相同工作负载的参考价格。

对于流式聊天回复,MiniMax-M2.7 的价格为每 1M 输入 token $0.05、每 1M 输出 token $0.15,上下文为 180K,每个答案最多 8,192 个输出 token。价格在任何时段都按模型固定,一个余额覆盖所有模型,因此只需切换 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 可在同一余额和同一 base URL 上使用。
  • 获取密钥并运行脚本:创建 CheapTokens API 密钥 并充值余额。

每个模型的端点、模型 id 和限制都在 CheapTokens 文档 中。