API & SDKGuide
Stream chat responses in Node.js with the OpenAI SDK
Stream a chat completion token by token in Node.js: for await loop, stopping on demand, error handling and token usage with the OpenAI SDK.

On this page
Streaming lets your app show the answer as it is generated instead of waiting for the whole reply. In this guide you will build a small Node.js script that streams a chat completion from the CheapTokens API token by token, stops it on demand and handles errors. The Python version of this topic is in Streaming responses in Python.
What you need#
- Node.js 22 or newer (the minimum version stated in the
openaipackage README, checked 22 October 2026). - A CheapTokens API key. If you do not have one yet, follow Your first API request with CheapTokens.
- The official SDK:
npm install openai.
CheapTokens exposes an OpenAI-compatible API at https:/, so the official openai package works by changing only the base URL and the key. Keep the key in an environment variable, not in the code:
export CHEAPTOKENS_API_KEY="your-key"Step 1. Stream the answer with for await#
Create stream.mjs (the .mjs extension lets you use import and top-level await):
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);Run it with node stream.mjs. The SDK reads the Server-Sent Events from the API and hands you one chunk per iteration of for await. Three details matter:
- Text arrives in
chunk.. Some chunks carry no text (the first one often holds only the role), so check thatchoices[0]. delta. content deltais not empty. process.prints without a line break, so the words appear one after another.stdout. write - With
stream_the last chunk carries aoptions: { include_ usage: true } usageobject with prompt and completion token counts, and itschoicesarray is empty. That is why the code useschunk.with the optional chaining.choices[0]?.
The model id comes from the CheapTokens documentation: providers name the same model differently, so copy the id from there.
Step 2. Stop the stream on demand#
Users press Stop, close a tab or switch chats. Cancel the request so you do not wait for tokens nobody will read. Pass an AbortSignal in the request options:
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 },
);The stream object also has its own stream., and leaving the for await loop with break closes the connection too. Use whichever is closer to your code: a signal when the cancel button lives elsewhere, break when the decision is made inside the loop (for example, you found a stop word).
Step 3. Handle errors#
A stream can fail before the first token (wrong key, empty balance) or in the middle (network drop). Wrap both the request and the loop in one try:
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;
}
}APIUserAbortErroris what you get after your ownabort(). It is not a failure, so do not show it as one.APIErrorhas the HTTPstatus: 401 means a wrong or missing key, 429 means too many requests, and a message about tokens means the balance needs a top-up. More causes and fixes are in Common connection errors and how to fix them and "Not enough tokens" error.- Text that has already been printed stays valid after an error. Keep it if you want to show the partial answer.
Step 4. Read usage and estimate cost#
The usage object holds prompt_tokens and completion_. Together with the model price per 1M tokens, it gives you the exact cost of a request. For a rough estimate before you run anything, use the live table below: it shows the cost of 1,000 requests with 2,000 input and 800 output tokens each.
| Model | Input $/1M | Output $/1M | Per request | 1,000 requests | OpenRouter, 1,000 requests |
|---|---|---|---|---|---|
| GLM-5.3-Flash | $0.02 | $0.04 | $0.000072 | $0.072 | $0.143 |
| DeepSeek-V4-Flash | $0.015 | $0.10 | $0.00011 | $0.11 | $0.22 |
| MiniMax-M2.7 | $0.05 | $0.15 | $0.00022 | $0.22 | $1.56 |
2,000 input and 800 output tokens per request. Prices per 1M tokens are current and change together with our tariffs; the OpenRouter column shows reference prices for the same workload.
For streamed chat replies, MiniMax-M2.7 is priced at $0.05 per 1M input and $0.15 per 1M output tokens, with a 180K context and up to 8,192 output tokens per answer. The price is fixed per model at any time of day, and one balance covers all models, so switching the model string is enough to compare them on your own prompts.
Full script#
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;
}
}Next steps#
- Send the chunks to a browser with Server-Sent Events from your own endpoint, so the key never leaves the server.
- Try other models by changing only the
modelstring: MiniMax-M2.7 is available on the same balance and the same base URL. - Get a key and run the script: create a CheapTokens API key and top up the balance.
Endpoints, model ids and limits for every model are in the CheapTokens docs.



