n4nAI

Building a CLI tool with the OpenAI Node.js SDK

Step-by-step tutorial for building a command-line interface tool with the OpenAI Node.js SDK, from setup to streaming responses and error handling.

n4n Team3 min read630 words

Audio narration

Coming soon — every post will get a voice note here.

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 fetch is 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.

Tagsnodejsopenai-sdkclitutorial

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All node.js openai-compatible sdk integration posts →