Custom base URL in Node.js — OpenAI and Anthropic SDKs
Pointing either SDK at a different endpoint is one constructor argument. The failures are almost never about the URL itself — they are about capitalisation, trailing slashes and env vars that never loaded.
https://aicomp.ai/v1).
Create one free →
curl https://aicomp.ai/v1/models \
-H "Authorization: Bearer sk-your-gateway-key"
A JSON list of model IDs means the key is good. Invalid token means it was copied wrong.
Environment first
Read config once, before any client is constructed. The most common failure in bundled frameworks is importing the SDK before dotenv has run, so the key is undefined at construction time and no amount of later fixing helps.
# .env — note there is no trailing /v1 here if your base already ends with it
GATEWAY_BASE=https://aicomp.ai/v1
GATEWAY_KEY=sk-your-gateway-key
# load order matters: dotenv must run BEFORE the client is constructed
import "dotenv/config";
OpenAI SDK
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.GATEWAY_KEY,
baseURL: process.env.GATEWAY_BASE ?? "https://aicomp.ai/v1",
});
const res = await client.chat.completions.create({
model: "gpt-5.6-terra",
messages: [{ role: "user", content: "Say OK" }],
max_tokens: 64,
});
console.log(res.choices[0].message.content);
The whole change is baseURL. Everything else — models, streaming, tools, structured output — is exactly as it was against OpenAI directly.
Anthropic SDK
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({
apiKey: process.env.GATEWAY_KEY,
baseURL: process.env.GATEWAY_BASE, // Anthropic needs this explicitly
});
// Unlike OpenAI-shaped APIs, max_tokens is REQUIRED on Anthropic.
const msg = await client.messages.create({
model: "claude-sonnet-5",
max_tokens: 256,
messages: [{ role: "user", content: "Say OK" }],
});
console.log(msg.content[0].text);
One difference catches everyone: max_tokens is required on Anthropic requests. Leaving it out is an API error, not a default. The system prompt also moves out of the messages array and into a top-level parameter.
No SDK at all
For verification, or in runtimes where installing a full SDK is overkill, plain fetch is enough. Note the trailing-slash strip — it is what prevents the doubled-path 404:
const base = process.env.GATEWAY_BASE.replace(/\/$/, ""); // strip trailing slash
const res = await fetch(`${base}/chat/completions`, {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.GATEWAY_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "gpt-5.6-luna",
messages: [{ role: "user", content: "Say OK" }],
max_tokens: 64,
}),
});
console.log(await res.json());
Rates for the models used above
| Model | Gateway rate in / out per 1M tokens | Official list in / out per 1M tokens | Diff |
|---|---|---|---|
| gpt-5.6-terra | $1 / $6 | $2 / $12 | 50% |
| gpt-5.6-luna | $0.1 / $0.6 | $0.2 / $1.2 | 50% |
| claude-sonnet-5 | $1 / $5 | $2 / $10 | 50% |
Rates checked 2026-09-16. Gateway rates move with upstream promotions — verify the current number in your dashboard before committing to a budget.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
| Requests still go to api.openai.com | baseUrl instead of baseURL — silently ignored. |
404 with /v1/v1/ in the path | Doubled /v1. Strip the trailing slash and check what the SDK appends. |
| 401, key 'invalid' | Truncated on copy, or env var undefined in this runtime. Log its length first. |
| Anthropic: 'max_tokens is required' | Not optional on that API. Set it on every request. |
| Everything arrives at once when streaming | Endpoint buffers instead of streaming. Harmless, but no time-to-first-token gain. |
FAQ
What is the correct option name in Node?
baseURL — with URL capitalised — for both the OpenAI and Anthropic SDKs. baseUrl (lowercase rl) is silently ignored by most versions, which is the single most common reason this 'does not work'.
Why do I get a 401 with a key I just copied?
Almost always a truncated key or an environment variable that never reached the process. Print the key length before constructing the client; if your framework bundles separately, confirm the env file is actually loaded in that runtime.
Do I put /v1 in the base URL?
Follow whatever the provider documents, then check what the SDK appends. The OpenAI SDK appends /chat/completions, so the base should already end in /v1. Doubling it produces a 404 with a path like /v1/v1/chat/completions. Strip any trailing slash to be safe.
Does streaming work in Node?
Yes, if the endpoint supports server-sent events. Pass stream: true and iterate the async chunks. If everything arrives at once, the endpoint is buffering rather than streaming.
Does the Anthropic SDK need anything different?
Two things: max_tokens is required on every request, and the system prompt is a top-level parameter rather than a system message inside the array.