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:
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

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-09-16. 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.

Related

Get API access