Full Node.js example: official SDKs through our gateway

The Node builds of the official SDKs configure exactly like their Python counterparts — one baseURL field reroutes the whole request.

2 min · · Updated · development

The Node packages @anthropic-ai/sdk and openai are built the same way as their Python counterparts: the client constructor accepts a baseURL, and everything downstream — streaming, tool use, error handling — works unchanged.

Install

bash
npm install @anthropic-ai/sdk openai

Anthropic SDK client

bash
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic({
  baseURL: "https://api.zukko.pro",
  apiKey: "fetch_your_key",
});

const message = await client.messages.create({
  model: "claude-sonnet-5",
  max_tokens: 1024,
  messages: [{ role: "user", content: "Explain the difference between var, let, and const" }],
});
console.log(message.content[0].text);

baseURL has no /v1 — the SDK appends /v1/messages itself. That's different from the OpenAI client below, and it's the single most common source of confusion on first setup: copy the URL from the OpenAI client's docs, paste it into the Anthropic client, and every request 404s.

OpenAI SDK client

bash
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api.zukko.pro/v1",
  apiKey: "fetch_your_key",
});

const response = await client.chat.completions.create({
  model: "gpt-5.6-sol",
  messages: [{ role: "user", content: "Explain the difference between var, let, and const" }],
});
console.log(response.choices[0].message.content);

Streaming

Anthropic SDK:

bash
const stream = client.messages.stream({
  model: "claude-sonnet-5",
  max_tokens: 1024,
  messages: [{ role: "user", content: "Write a haiku about deadlines" }],
});

stream.on("text", (text) => process.stdout.write(text));
await stream.finalMessage();

OpenAI SDK:

bash
const stream = await client.chat.completions.create({
  model: "gpt-5.6-sol",
  messages: [{ role: "user", content: "Write a haiku about deadlines" }],
  stream: true,
});

for await (const chunk of stream) {
  const delta = chunk.choices[0]?.delta?.content;
  if (delta) process.stdout.write(delta);
}

Tool use / function calling

Through the Responses API (openai SDK, /v1/responses endpoint):

bash
const response = await client.responses.create({
  model: "gpt-5.6-sol",
  input: "What's the weather in Almaty?",
  tools: [{
    type: "function",
    name: "get_weather",
    description: "Returns the current weather for a city",
    parameters: {
      type: "object",
      properties: { city: { type: "string" } },
      required: ["city"],
    },
  }],
});

const call = response.output.find((item) => item.type === "function_call");
if (call) {
  console.log("Model wants to call:", call.name, JSON.parse(call.arguments));
}

You could just as well put claude-sonnet-5 or glm-5.3 here instead of gpt-5.6-sol — /v1/responses behaves identically regardless of provider, because routing happens on the gateway side, not the client's. The current model list is at GET https://api.zukko.pro/v1/models.

Vision in Node

bash
import { readFileSync } from "fs";

const imageBase64 = readFileSync("screenshot.png").toString("base64");

const message = await client.messages.create({
  model: "claude-opus-5-5",
  max_tokens: 1024,
  messages: [{
    role: "user",
    content: [
      { type: "image", source: { type: "base64", media_type: "image/png", data: imageBase64 } },
      { type: "text", text: "Describe what's in this screenshot" },
    ],
  }],
});
console.log(message.content[0].text);

Error handling

bash
import Anthropic from "@anthropic-ai/sdk";

try {
  await client.messages.create({
    model: "claude-sonnet-5",
    max_tokens: 100,
    messages: [{ role: "user", content: "test" }],
  });
} catch (err) {
  if (err instanceof Anthropic.AuthenticationError) {
    console.error("Invalid key — check Bearer fetch_...");
  } else if (err instanceof Anthropic.APIError && err.status === 402) {
    console.error("Balance is empty:", err.error);
  } else if (err instanceof Anthropic.APIError && err.status === 429) {
    console.error("Key limit (key_daily/monthly_limit_reached) or a 429 from the provider");
  } else {
    throw err;
  }
}

The openai SDK follows the same pattern: err instanceof OpenAI.APIError with an err.status check, including codes nonstandard for the original API like 402 and 422.

Idempotency-Key and cost headers

bash
const response = await client.messages.create(
  {
    model: "claude-sonnet-5",
    max_tokens: 100,
    messages: [{ role: "user", content: "test" }],
  },
  { headers: { "Idempotency-Key": "invoice-job-77" } }
);

The key goes into the options of the specific call — unique per logical request and identical across its retries. Don't set it in the client's defaultHeaders: the very next request with a different body would get 422 idempotency_conflict.

To read X-Zukko-Cost-Micros and X-Fetch-Balance-Cents you need the raw response (the headers are only present on regular, non-streaming responses; X-Fetch-Balance-Cents, despite the name, is in micro-dollars, 1,000,000 = $1):

bash
const { data, response: raw } = await client.messages
  .create({
    model: "claude-sonnet-5",
    max_tokens: 100,
    messages: [{ role: "user", content: "test" }],
  })
  .withResponse();

console.log(raw.headers.get("x-zukko-cost-micros"));
console.log(raw.headers.get("x-fetch-balance-cents"));

TypeScript and response types

Both SDKs ship with types out of the box — no extra @types/* packages needed:

bash
import Anthropic from "@anthropic-ai/sdk";
import type { MessageParam } from "@anthropic-ai/sdk/resources/messages";

const client = new Anthropic({
  baseURL: "https://api.zukko.pro",
  apiKey: process.env.ZUKKO_API_KEY,
});

const history: MessageParam[] = [
  { role: "user", content: "Draft a migration plan from Express to Fastify" },
];

const message = await client.messages.create({
  model: "claude-opus-5-5",
  max_tokens: 2048,
  messages: history,
});

The MessageParam type comes straight from the @anthropic-ai/sdk package rather than being hand-written — handy when the message history is assembled dynamically inside a conversation loop, since the compiler flags a malformed object before the code ever runs.

Timeouts and mid-stream cutoffs

In Node, a response cutting off mid-way is usually the default fetch or SDK timeout, not a token limit enforced by the gateway. Both clients accept timeout in milliseconds:

bash
const client = new Anthropic({
  baseURL: "https://api.zukko.pro",
  apiKey: "fetch_your_key",
  timeout: 120000,
});

If a long generation reliably cuts off at the same spot, check max_tokens in the request first, not your network settings — the model may simply have hit the output limit you set yourself. And keep in mind: a client-side disconnect doesn't stop the charge — the gateway reads the provider's response to the end and bills the whole generation.

zukko

Pay 25×
less

for Claude Code & ChatGPT

One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.

Get API keyDocumentation

api.zukko.pro · fetch_*

Node.js @anthropic-ai/sdk and openai via our gateway · Zukko