Short answer

Install the openai package, read the base URL, key and model from the environment, and call client.chat.completions.create. Streaming works with for await, and an AbortController cancels a slow request. The same code runs against hosted providers and local OpenAI-compatible servers by changing three environment variables.

Test status, 14 November 2026: Every Node.js sample on this page was run unchanged against the mock OpenAI-compatible server in this site's repository, including two 429 responses that the SDK retried and a missing key that the code reported. Environment: Node.js 22.19.0, openai 7.57.0 for Node, Windows 10. Not yet run against a live provider endpoint.

Set up

You need Node.js 22 or newer for the current version of the openai package, an API key (or a local server such as llama.cpp, vLLM or Ollama), and the model id your provider expects.

bash code
mkdir llm-demo && cd llm-demo
npm init -y
npm pkg set type=module
npm install openai

Set three environment variables instead of hard-coding anything. The values below are one documented example; see the API overview for others.

bash code
export LLM_BASE_URL="https://api.venice.ai/api/v1"
export LLM_API_KEY="your-key-here"
export LLM_MODEL="venice-uncensored-1-2"

With Node.js 20.6 or newer you can also keep them in a .env file that is excluded from version control and run node --env-file=.env chat.mjs.

First request

javascript code
// One chat completion against any OpenAI-compatible API.
// Set LLM_BASE_URL, LLM_API_KEY and LLM_MODEL in the environment first.
import OpenAI from 'openai'

const client = new OpenAI({
  baseURL: process.env.LLM_BASE_URL,
  apiKey: process.env.LLM_API_KEY,
  timeout: 60_000, // milliseconds for the whole request
  maxRetries: 2, // the SDK retries 408, 409, 429 and 5xx with backoff
})

try {
  const reply = await client.chat.completions.create({
    model: process.env.LLM_MODEL,
    messages: [
      { role: 'system', content: 'You are a concise assistant for developers.' },
      { role: 'user', content: 'Explain what a race condition is in two sentences.' },
    ],
    max_tokens: 300,
  })
  const choice = reply.choices[0]
  console.log(choice.message.content)
  console.log(`finish_reason=${choice.finish_reason}`)
  if (reply.usage) console.log(`tokens: ${reply.usage.prompt_tokens} in, ${reply.usage.completion_tokens} out`)
} catch (err) {
  if (err instanceof OpenAI.APIError && err.status) {
    // 401 bad key, 404 wrong base URL or model, 413 prompt too long, 429 rate limit
    console.error(`The API returned ${err.status}: ${err.message}`)
  } else {
    console.error(`Request failed: ${err.message}`)
  }
  process.exitCode = 1
}

The client posts to <base URL>/chat/completions with a bearer token. maxRetries: 2 makes the SDK retry connection errors, 408, 409, 429 and server errors with a short backoff; in our test, two 429 responses were retried and the third request succeeded. OpenAI.APIError carries the HTTP status, so the catch block can tell a bad key (401) from a rate limit (429).

Check finish_reason. A value of length means the reply was cut off by max_tokens or by the provider’s own output cap, which some providers apply whatever you send.

Streaming with a timeout

javascript code
// Stream a reply with the OpenAI SDK, with a timeout that cancels the request.
import OpenAI from 'openai'

const client = new OpenAI({ baseURL: process.env.LLM_BASE_URL, apiKey: process.env.LLM_API_KEY })
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), 30_000) // give up after 30 s

let text = ''
let usage = null
try {
  const stream = await client.chat.completions.create(
    {
      model: process.env.LLM_MODEL,
      messages: [{ role: 'user', content: 'Write a haiku about version control.' }],
      max_tokens: 200,
      stream: true,
      stream_options: { include_usage: true }, // some servers ignore this
    },
    { signal: controller.signal },
  )
  for await (const chunk of stream) {
    if (chunk.usage) usage = chunk.usage
    const piece = chunk.choices[0]?.delta?.content
    if (piece) {
      text += piece
      process.stdout.write(piece)
    }
  }
  process.stdout.write('\n')
  if (usage) console.log(`tokens: ${usage.prompt_tokens} in, ${usage.completion_tokens} out`)
  console.log(`characters received: ${text.length}`)
} catch (err) {
  console.error(controller.signal.aborted ? 'Timed out and cancelled.' : `Stream failed: ${err.message}`)
  process.exitCode = 1
} finally {
  clearTimeout(timer)
}

The SDK turns the server-sent event stream into an async iterator. Passing an AbortSignal lets you cancel: when the timer fires, the request is aborted, the loop throws, and the connection closes, so you stop paying for tokens you will not use. The same pattern works for a “stop generating” button.

stream_options: { include_usage: true } requests a final chunk with token counts. That chunk has an empty choices array, which is why the loop uses optional chaining. Some servers send usage regardless, and some never do.

Without the SDK

If you cannot add a dependency, or you want to see what the SDK does, the request is a plain fetch. The streaming guide shows a complete server-sent events parser that we ran against the same mock server.

Errors and limits

Symptom Likely cause
401 The key is missing in this process, or wrong
404 Base URL path is wrong (/v1 vs /api/v1) or the model id is not listed
413 or 400 about length Input plus reserved output exceed the context limit
429 Rate limit or quota; retries help only for short bursts
finish_reason: "length" with no text A thinking model spent its output budget on reasoning

Context limits are set per provider and are often lower than the model’s configuration; the context window planner helps you budget a request.

Next steps

Questions

Can I call the API from the browser?

Not with your own key: anything in browser code is visible to users. Call the provider from your server and send only the result to the browser.

Does this work with TypeScript?

Yes. The openai package ships its own types, so the same code works in a .ts file; the request and response objects are typed.

Which Node.js version do I need?

The current major version of the openai package declares Node.js 22 or newer. The fetch-based streaming example needs only Node.js 18 or newer.

Sources

  1. openai-node, GitHub (OpenAI)
  2. Chat Completions API reference, OpenAI
  3. Ollama OpenAI compatibility, Ollama

Found an error? Report it. We log material corrections publicly.