n4nAI

Using the OpenAI Node.js SDK with Claude Opus 4.1

Learn how to wire the OpenAI Node.js SDK to Claude Opus 4.1 via an OpenAI-compatible gateway, with runnable code for chat, streaming, and tools.

n4n Team3 min read610 words

Audio narration

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

The OpenAI Node.js SDK speaks a wire format that many inference gateways now mirror, which means you can drive Anthropic’s flagship without swapping clients. This tutorial builds a small Node script that calls openai node.js sdk claude opus 4.1 through an OpenAI-compatible endpoint, then extends it to streaming and tool use so you can drop it into a real service.

Prerequisites

  • Node.js 18 or newer (fetch and native stream support required).
  • An API key from a gateway that exposes Claude Opus 4.1 behind an OpenAI-compatible /v1 route.
  • The model identifier your gateway uses. In this guide we assume claude-opus-4.1.
  • Basic comfort with ES modules and async/await.

Set the key in your shell before running anything:

export N4N_API_KEY="sk-..."   # or whatever your gateway issues

Project setup

Create a scratch directory and install the SDK. The OpenAI package is the only dependency.

mkdir opus-test && cd opus-test
npm init -y
npm install openai@^4

Use "type": "module" in package.json so the import syntax below works without flags.

Configure the client

The trick is telling the SDK to ignore api.openai.com and hit your gateway instead. Everything else stays identical to a normal OpenAI call.

// client.mjs
import OpenAI from 'openai';

const client = new OpenAI({
  apiKey: process.env.N4N_API_KEY,
  baseURL: 'https://api.n4n.ai/v1', // OpenAI-compatible endpoint
});

export default client;

If your gateway uses a different host, change baseURL. The model string is the only Anthropic-specific detail.

First synchronous call

Write a minimal script that asks a direct question and prints the reply.

// chat.mjs
import client from './client.mjs';

const resp = await client.chat.completions.create({
  model: 'claude-opus-4.1',
  messages: [
    { role: 'system', content: 'You are a terse senior engineer.' },
    { role: 'user', content: 'What is the difference between a mutex and a semaphore?' },
  ],
});

console.log(resp.choices[0].message.content);
console.log('tokens:', resp.usage);

Run it:

node chat.mjs

Expected output (trimmed):

A mutex allows one thread to hold a lock; a semaphore permits N. Mutexes are for mutual exclusion, semaphores for counting resource slots.
tokens: { prompt_tokens: 28, completion_tokens: 31, total_tokens: 59 }

The usage object is standard OpenAI shape, so existing metering code works unchanged.

Streaming responses

For chat UIs you want tokens as they arrive. Flip stream: true and iterate the async iterator.

// stream.mjs
import client from './client.mjs';

const stream = await client.chat.completions.create({
  model: 'claude-opus-4.1',
  messages: [{ role: 'user', content: 'Write a haiku about TCP retransmits.' }],
  stream: true,
});

for await (const chunk of stream) {
  const delta = chunk.choices[0]?.delta?.content || '';
  process.stdout.write(delta);
}
process.stdout.write('\n');

Run node stream.mjs. You’ll see the poem appear word by word. The chunk structure is identical to OpenAI’s streaming API: chunk.choices[0].delta.content.

Tool calls

Claude Opus 4.1 handles function calling through the same JSON schema the OpenAI SDK expects. Define a tool and pass it in the request.

// tools.mjs
import client from './client.mjs';

const tools = [
  {
    type: 'function',
    function: {
      name: 'get_incident_status',
      parameters: {
        type: 'object',
        properties: {
          incident_id: { type: 'string' },
        },
        required: ['incident_id'],
      },
    },
  },
];

const resp = await client.chat.completions.create({
  model: 'claude-opus-4.1',
  messages: [{ role: 'user', content: 'Is incident INC-442 still open?' }],
  tools,
  tool_choice: 'auto',
});

const msg = resp.choices[0].message;
if (msg.tool_calls) {
  for (const call of msg.tool_calls) {
    console.log('calling', call.function.name, call.function.arguments);
    // Parse args, query your system, then return a tool message.
  }
} else {
  console.log(msg.content);
}

Sample output when the model decides to call:

calling get_incident_status {"incident_id":"INC-442"}

To close the loop, append the assistant message (with tool_calls) and a role: 'tool' message carrying the function result, then call create again. The OpenAI Node.js SDK serializes this correctly for the gateway.

const followup = await client.chat.completions.create({
  model: 'claude-opus-4.1',
  messages: [
    { role: 'user', content: 'Is incident INC-442 still open?' },
    msg,
    {
      role: 'tool',
      tool_call_id: msg.tool_calls[0].id,
      content: JSON.stringify({ status: 'resolved', duration_min: 12 }),
    },
  ],
  tools,
});

console.log(followup.choices[0].message.content);

Error handling and resilience

Providers throttle. Wrap calls so a 429 or 5xx doesn’t crash the process.

// safe.mjs
import client from './client.mjs';

async function complete(prompt) {
  try {
    return await client.chat.completions.create({
      model: 'claude-opus-4.1',
      messages: [{ role: 'user', content: prompt }],
      timeout: 20_000,
    });
  } catch (err) {
    if (err.status === 429) {
      console.error('rate limited, back off');
    } else if (err.status >= 500) {
      console.error('upstream degraded');
    } else {
      console.error('unexpected', err.message);
    }
    throw err;
  }
}

If you point the SDK at n4n.ai, the gateway handles automatic fallback when a provider is rate-limited or degraded, so the same code survives transient errors without you writing retry loops across vendors.

Usage metering and cost tracking

Every completion response includes usage. Pipe it into your own counters:

let promptTokens = 0;
let completionTokens = 0;

function tally(resp) {
  promptTokens += resp.usage.prompt_tokens;
  completionTokens += resp.usage.completion_tokens;
}

const r1 = await complete('Summarize RFC 9000');
tally(r1);

Because n4n.ai meters per token on that standard field, you can aggregate spend across Claude, GPT, and open-weight models in one ledger without custom parsers.

Putting it in a service

A realistic layout separates the client, the model config, and the call sites:

src/
  llm/client.mjs      # baseURL + key
  llm/chat.mjs        # chat + stream helpers
  llm/tools.mjs       # tool schemas
  routes/agent.mjs    # HTTP handler

Keep the model name in one constant. If you later switch to a different Claude revision or a competing model, you change one line.

export const PRIMARY_MODEL = 'claude-opus-4.1';

The OpenAI Node.js SDK’s TypeScript types still apply, so you get autocomplete on messages and tools even though the backend is Anthropic. That alone removes a class of schema bugs.

Gotchas

  • System messages: Claude accepts them via the OpenAI compat layer, but some gateways map them to a leading human turn. Test your system prompt separately.
  • Max tokens: Set max_tokens explicitly. Claude’s defaults differ from OpenAI’s, and the gateway passes the field through.
  • Stream termination: Always consume the full async iterator. Breaking early can leave sockets open under load.
  • Tool choice string: Use 'auto' or a specific { type: 'function', function: { name } } object. Passing true is deprecated in the SDK.

Final check

You now have runnable code that uses the OpenAI Node.js SDK to call Claude Opus 4.1 for sync chat, streaming, and tool-augmented responses. The surface area is small: one baseURL, one model string, and the standard chat.completions interface. Swap the endpoint and model ID and the same patterns cover every other model your gateway exposes.

Tagsnodejsopenai-sdkclauden4n-ai

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 →