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.

In short: Both major Node SDKs take baseURL (capitalised) on the constructor; baseUrl is silently ignored, which is the most common reason this appears not to work. Two other predictable failures: doubling the /v1 suffix produces a 404, and the Anthropic API requires max_tokens on every request.
You need a key before the code below runs. Create an account, generate a key, and copy the base URL (https://aicomp.ai/v1). Create one free →
Check current rates → Free to sign up · $1 minimum top-up · No prepayment
Confirm the key works first. One command, no SDK, costs nothing:
Don't have an account yet? You need an API key from the gateway before the steps below will work — how the discount works and what you take on. Create one free → (no prepay, $1 minimum top-up)
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

Rates for the models used above — USD per 1M tokens
ModelGateway rate
in / out per 1M tokens
Official list
in / out per 1M tokens
Diff
gpt-5.6-terra$1 / $6$2 / $1250%
gpt-5.6-luna$0.1 / $0.6$0.2 / $1.250%
claude-sonnet-5$1 / $5$2 / $1050%

Rates checked 2026-11-01. Gateway rates move with upstream promotions, verify the current number in your dashboard before committing to a budget.

Cost note. Output costs several times more than input on every model here, so setting max_tokens is not just an Anthropic requirement It is the cheapest single guardrail you can add in Node. Pick a value around twice your expected response length, not ten times it.

Troubleshooting

SymptomCause and fix
Requests still go to api.openai.combaseUrl instead of baseURL, silently ignored.
404 with /v1/v1/ in the pathDoubled /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 streamingEndpoint 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

Get API access

Rate cards: GPT and Claude