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, the two SDKs need DIFFERENT bases. The OpenAI SDK sends
# /chat/completions and takes the versioned base; the Anthropic SDK appends
# /v1/messages itself and therefore takes the bare host.
GATEWAY_BASE=https://aicomp.ai/v1
GATEWAY_BASE_ANTHROPIC=https://aicomp.ai
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 ?? "https://aicomp.ai", // no /v1: the SDK appends /v1/messages
});
// 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);
Two differences catch everyone. First the base URL: it is not the same value as the OpenAI one above — reuse GATEWAY_BASE here and every request goes to /v1/v1/messages, which returns 404. Second, 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-11-01. 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.
How do I set the OpenAI base URL in Node.js?
Pass it when constructing the client: new OpenAI({ baseURL: process.env.OPENAI_BASE_URL }). Note the casing — Node uses baseURL while Python uses base_url, and getting it wrong fails silently by falling back to the vendor default, which looks like your override simply did nothing. Keep the trailing /v1: the client appends the resource path, not the version.
Can I read the base URL from an environment variable in Node?
Yes, and it is the better default — read process.env.OPENAI_BASE_URL at construction time with the vendor default as a fallback. Changing route then becomes a config change rather than a deploy, and you can point staging and production at different endpoints from the same code. One caveat: read it once at startup, not per request, so a late mutation cannot silently retarget live traffic.
Related
- The same thing in Python
- The same pattern for Gemini — exact model IDs and the OpenAI-compatible route
- OpenAI SDK custom endpoint — general form
- What 'OpenAI-compatible' actually covers
- Point LiteLLM, Vercel AI SDK or Cline at the same endpoint
- Error codes diagnosed