Quay lại blog

API & SDKHướng dẫn

Stream phản hồi chat trong Node.js với OpenAI SDK

Stream một chat completion theo từng token trong Node.js: vòng lặp for await, dừng theo yêu cầu, xử lý lỗi và sử dụng token với OpenAI SDK.

Bởi CheapTokens TeamĐã xuất bản Đã cập nhật 5 phút đọc

Cửa sổ terminal in một câu trả lời được stream theo từng token từ một script Node.js
Trên trang này
  1. Những gì bạn cần
  2. Bước 1. Stream câu trả lời với for await
  3. Bước 2. Dừng stream theo yêu cầu
  4. Bước 3. Xử lý lỗi
  5. Bước 4. Đọc usage và ước tính chi phí
  6. Script đầy đủ
  7. Các bước tiếp theo

Streaming cho phép ứng dụng của bạn hiển thị câu trả lời ngay khi nó được tạo ra thay vì chờ toàn bộ phản hồi. Trong hướng dẫn này, bạn sẽ xây dựng một script Node.js nhỏ để stream một chat completion từ API CheapTokens theo từng token, dừng nó theo yêu cầu và xử lý lỗi. Phiên bản Python của chủ đề này nằm trong Streaming responses in Python.

Những gì bạn cần#

  • Node.js 22 trở lên (phiên bản tối thiểu được nêu trong README của gói openai, đã kiểm tra ngày 22 tháng 10 năm 2026).
  • Một khóa API CheapTokens. Nếu bạn chưa có, hãy làm theo Your first API request with CheapTokens.
  • SDK chính thức: npm install openai.

CheapTokens cung cấp API tương thích OpenAI tại https://api.cheaptokens.dev/v1, vì vậy gói openai chính thức hoạt động chỉ bằng cách thay đổi base URL và khóa. Giữ khóa trong một biến môi trường, không phải trong mã:

bash
export CHEAPTOKENS_API_KEY="your-key"

Bước 1. Stream câu trả lời với for await#

Tạo stream.mjs (phần mở rộng .mjs cho phép bạn sử dụng import và await cấp cao nhất):

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);

Chạy nó với node stream.mjs. SDK đọc Server-Sent Events từ API và đưa cho bạn một chunk mỗi lần lặp của for await. Ba chi tiết quan trọng:

  • Văn bản đến trong chunk.choices[0].delta.content. Một số chunk không mang văn bản (chunk đầu tiên thường chỉ chứa role), vì vậy hãy kiểm tra rằng delta không rỗng.
  • process.stdout.write in mà không có ngắt dòng, vì vậy các từ xuất hiện lần lượt.
  • Với stream_options: { include_usage: true }, chunk cuối cùng mang một đối tượng usage với số lượng token prompt và completion, và mảng choices của nó rỗng. Đó là lý do mã sử dụng chunk.choices[0]?. với optional chaining.

Id model đến từ tài liệu CheapTokens: các nhà cung cấp đặt tên cùng một model khác nhau, vì vậy hãy sao chép id từ đó.

Bước 2. Dừng stream theo yêu cầu#

Người dùng nhấn Stop, đóng tab hoặc chuyển cuộc trò chuyện. Hủy yêu cầu để bạn không phải chờ các token mà không ai đọc. Truyền một AbortSignal trong tùy chọn yêu cầu:

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 },
);

Đối tượng stream cũng có stream.controller.abort() riêng, và thoát khỏi vòng lặp for await với break cũng đóng kết nối. Sử dụng cái nào gần mã của bạn hơn: một signal khi nút hủy nằm ở nơi khác, break khi quyết định được đưa ra bên trong vòng lặp (ví dụ: bạn tìm thấy một từ dừng).

Bước 3. Xử lý lỗi#

Một stream có thể thất bại trước token đầu tiên (sai khóa, số dư trống) hoặc ở giữa (mất mạng). Bao bọc cả yêu cầu và vòng lặp trong một 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 là những gì bạn nhận được sau abort() của riêng bạn. Đó không phải là lỗi, vì vậy đừng hiển thị nó như một lỗi.
  • APIError có status HTTP: 401 nghĩa là sai hoặc thiếu khóa, 429 nghĩa là quá nhiều yêu cầu, và thông báo về token nghĩa là số dư cần được nạp thêm. Nguyên nhân và cách khắc phục khác có trong Common connection errors and how to fix them và "Not enough tokens" error.
  • Văn bản đã được in vẫn hợp lệ sau một lỗi. Giữ nó nếu bạn muốn hiển thị câu trả lời một phần.

Bước 4. Đọc usage và ước tính chi phí#

Đối tượng usage chứa prompt_tokens và completion_tokens. Cùng với giá model trên 1 triệu token, nó cho bạn chi phí chính xác của một yêu cầu. Để có ước tính sơ bộ trước khi chạy bất cứ điều gì, hãy sử dụng bảng trực tiếp bên dưới: nó hiển thị chi phí của 1.000 yêu cầu với 2.000 token đầu vào và 800 token đầu ra mỗi yêu cầu.

Mô hình Mỗi yêu cầu 1.000 yêu cầu OpenRouter, 1.000 yêu cầu
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 đầu vào và 800 token đầu ra mỗi yêu cầu. Giá trên 1 triệu token là giá hiện hành và thay đổi cùng với biểu phí của chúng tôi; cột OpenRouter hiển thị giá tham chiếu cho cùng khối lượng công việc.

Đối với các câu trả lời chat được stream, MiniMax-M2.7 có giá $0.05 trên 1 triệu token đầu vào và $0.15 trên 1 triệu token đầu ra, với ngữ cảnh 180K và tối đa 8.192 token đầu ra mỗi câu trả lời. Giá được cố định cho mỗi model vào bất kỳ thời điểm nào trong ngày, và một số dư bao gồm tất cả các model, vì vậy chỉ cần thay đổi chuỗi model là đủ để so sánh chúng trên các prompt của riêng bạn.

Script đầy đủ#

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;
  }
}

Các bước tiếp theo#

  • Gửi các chunk đến trình duyệt với Server-Sent Events từ endpoint của riêng bạn, để khóa không bao giờ rời khỏi máy chủ.
  • Thử các model khác chỉ bằng cách thay đổi chuỗi model: MiniMax-M2.7 có sẵn trên cùng số dư và cùng base URL.
  • Lấy khóa và chạy script: create a CheapTokens API key và nạp thêm số dư.

Các endpoint, id model và giới hạn cho mọi model có trong CheapTokens docs.