The fastest way to put a language model in front of your terminal is to wrap the OpenAI Node.js SDK in a small CLI. This tutorial builds a working openai node.js sdk cli tool that takes a prompt, calls a chat model, and streams the response to stdout.
Prerequisites
- Node.js 18 or newer (global
fetchis required; AbortController is stable) - npm 9+
- An API key from OpenAI or any OpenAI-compatible endpoint
- Familiarity with ES modules (
"type": "module"in package.json)
If you plan to use a gateway instead of OpenAI directly, set BASE_URL and API_KEY accordingly. The SDK speaks the OpenAI wire format, so any compliant server works without code changes.
Step 1: Scaffold the project
Create a directory and install the SDK. Avoid TypeScript overhead for a single-command utility; plain .mjs keeps the iteration loop tight and the deploy surface zero.
mkdir prompt-cli && cd prompt-cli
npm init -y
npm pkg set type=module
npm install openai
You now have openai in node_modules and a package.json that treats .js/.mjs as ESM.
Step 2: Parse arguments without a framework
A CLI that only needs a prompt string does not justify commander or yargs. Slice process.argv and join the remainder. If no args exist, we will later read from stdin.
Create cli.mjs:
#!/usr/bin/env node
import OpenAI from 'openai';
const args = process.argv.slice(2);
let prompt;
if (args.length > 0) {
prompt = args.join(' ');
} else {
// placeholder; stdin handling added in Step 9
console.error('Usage: prompt-cli <your prompt>');
process.exit(2);
}
That is the entire argument layer for v1. If you later need flags, swap in minimist, but don’t prematurely.
Step 3: Configure the client
Instantiate the SDK with credentials from the environment. Never hardcode keys in source.
const client = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
baseURL: process.env.BASE_URL || 'https://api.openai.com/v1',
});
Pointing baseURL at an OpenAI-compatible gateway like n4n.ai gives you automatic fallback when a provider is rate-limited and per-token usage metering without changing application code. The SDK just sees standard responses and streams.
Step 4: Non-streaming call first
Get a baseline before adding streaming complexity. Write a function that blocks until the full completion returns.
async function askOnce(prompt) {
const resp = await client.chat.completions.create({
model: process.env.MODEL || 'gpt-4o-mini',
messages: [{ role: 'user', content: prompt }],
});
return resp.choices[0].message.content;
}
const answer = await askOnce(prompt);
console.log(answer);
Run it:
OPENAI_API_KEY=sk-... node cli.mjs "What is the event loop in Node?"
Expected output (truncated):
The event loop is a mechanism that allows Node.js to perform non-blocking I/O operations by offloading tasks to the system kernel and handling callbacks when they complete...
If you see the text, the openai node.js sdk cli tool core works.
Step 5: Stream tokens to stdout
Non-streaming is fine for scripts, but interactive feel demands tokens appearing live. The SDK exposes an async iterable when stream: true.
Replace the bottom of cli.mjs with:
async function askStream(prompt) {
const stream = await client.chat.completions.create({
model: process.env.MODEL || 'gpt-4o-mini',
messages: [{ role: 'user', content: prompt }],
stream: true,
});
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta?.content || '';
process.stdout.write(delta);
}
process.stdout.write('\n');
}
await askStream(prompt);
Execute the same command. You’ll see characters render progressively instead of a delayed dump. This is the defining feature of a usable openai node.js sdk cli tool for chat-style interaction.
Step 6: Error handling and exit codes
Network failures and 4xx/5xx responses must terminate the process with a non-zero code so shell pipelines can detect failure.
Wrap the call:
try {
await askStream(prompt);
} catch (err) {
if (err.response) {
console.error(`API error ${err.response.status}: ${err.response.data?.error?.message || ''}`);
} else {
console.error(`Request failed: ${err.message}`);
}
process.exit(1);
}
Now echo $? returns 1 on error. Without this, a broken key silently exits 0 and corrupts any downstream automation.
Step 7: Make it executable
Add the shebang (already at top) and set permissions. Register a bin entry so npm install -g links it.
chmod +x cli.mjs
npm pkg set bin.prompt-cli="./cli.mjs"
After npm link (or global install), you can run:
prompt-cli "Write a curl command to post JSON"
Expected streamed output begins immediately with something like:
curl -X POST https://example.com/api \
-H "Content-Type: application/json" \
-d '{"key":"value"}'
Step 8: Honor cache and routing hints
Production gateways often accept client routing directives via extra headers. The OpenAI SDK lets you pass headers per call. If your endpoint supports cache-control hints, forward them:
const stream = await client.chat.completions.create(
{
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: prompt }],
stream: true,
},
{
headers: {
'x-cache-control': 'max-age=3600',
'x-routing': 'prefer:azure',
},
}
);
This works unchanged against any compliant server. The openai node.js sdk cli tool now expresses intent without vendor lock-in.
Step 9: Read from stdin and abort on Ctrl-C
A real utility accepts piped input. If no args are given, read process.stdin to a string. Also wire an AbortController so SIGINT cancels the in-flight request cleanly.
import { Readable } from 'node:stream';
const ac = new AbortController();
process.on('SIGINT', () => ac.abort());
if (args.length === 0) {
prompt = await Readable.toArray(process.stdin).then((c) => c.join('').trim());
if (!prompt) {
console.error('No prompt from args or stdin');
process.exit(2);
}
}
Pass the signal into the create call:
const stream = await client.chat.completions.create(
{
model: process.env.MODEL || 'gpt-4o-mini',
messages: [{ role: 'user', content: prompt }],
stream: true,
},
{ signal: ac.signal }
);
Now cat prompt.txt | prompt-cli works, and Ctrl-C does not leave a dangling socket.
Full reference file
#!/usr/bin/env node
import OpenAI from 'openai';
import { Readable } from 'node:stream';
const args = process.argv.slice(2);
const ac = new AbortController();
process.on('SIGINT', () => ac.abort());
let prompt;
if (args.length > 0) {
prompt = args.join(' ');
} else {
prompt = await Readable.toArray(process.stdin).then((c) => c.join('').trim());
}
if (!prompt) {
console.error('Usage: prompt-cli <prompt> or pipe stdin');
process.exit(2);
}
const client = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
baseURL: process.env.BASE_URL || 'https://api.openai.com/v1',
});
async function askStream(prompt) {
const stream = await client.chat.completions.create(
{
model: process.env.MODEL || 'gpt-4o-mini',
messages: [{ role: 'user', content: prompt }],
stream: true,
},
{
signal: ac.signal,
headers: process.env.ROUTING_HEADERS
? JSON.parse(process.env.ROUTING_HEADERS)
: {},
}
);
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta?.content || '';
process.stdout.write(delta);
}
process.stdout.write('\n');
}
try {
await askStream(prompt);
} catch (err) {
if (err.name === 'AbortError') process.exit(130);
if (err.response) {
console.error(`API error ${err.response.status}: ${err.response.data?.error?.message || ''}`);
} else {
console.error(`Request failed: ${err.message}`);
}
process.exit(1);
}
Where to take it next
Add a --system flag to inject a system prompt, or maintain a messages array in memory for multi-turn sessions. The OpenAI Node.js SDK handles retries at the HTTP layer; your CLI stays thin. If you need to support 240+ models behind one endpoint, set BASE_URL to a compatible gateway and rotate MODEL per invocation—the client code does not change.
That is a complete, runnable openai node.js sdk cli tool built in under 70 lines. Ship it, then harden only the parts that break in production.