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
npm install @anthropic-ai/sdk openaiAnthropic SDK client
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
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:
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:
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):
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
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
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
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):
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:
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:
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.
Related articles
- Prompt Injection: What It Is and How to Protect a Bot and an AgentHow prompt injection works through user input and third-party documents, and which measures limit the damage: data isolation, tool permissions, key limits.
- Your Own Neural Network Proxy on Node.js: Keep the API Key Off the FrontendA small Express server between your frontend and the model API: the key stays on the server, and models, request size and rate are capped.
- DeepSeek API in Python: Requests, Streaming, JSON and Error HandlingReady code for DeepSeek V4.1 Flash and V4 Pro on the OpenAI SDK: a request, streaming, JSON parsing and careful error handling.
- Prompt Engineering Examples: 12 Ready-Made Templates for WorkTwelve prompt templates for everyday tasks: what each one says, why it is built that way, and what a single API call costs.