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
/v1route. - 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_tokensexplicitly. 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. Passingtrueis 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.