Node.js
Node.js examples with the Anthropic and OpenAI SDKs - a plain request, streaming, a tool call round trip, an image, counting tokens, listing models and handling errors.
When to use this page
You are calling EZAT Router from Node.js or TypeScript with the official Anthropic SDK (@anthropic-ai/sdk) or OpenAI SDK (openai). The raw HTTP requests are on curl.
Set up
Install the SDK you want (or both), and put your key in an environment variable:
npm install @anthropic-ai/sdk openai
export EZAT_API_KEY="ezat_sk_…"The samples are ES modules in TypeScript with top-level await. Save one as example.mts and run node example.mts (Node.js 22.18 or later runs TypeScript directly), or drop the type annotations and save it as example.mjs.
Each SDK needs only its base URL changed:
| SDK | baseURL | Endpoint it calls |
|---|---|---|
@anthropic-ai/sdk | https://api.router.ezat.io | /v1/messages |
openai | https://api.router.ezat.io/openai/v1 | /openai/v1/chat/completions |
Both SDKs also read the base URL and key from the environment, which is handy for tools you don't write yourself: ANTHROPIC_BASE_URL and ANTHROPIC_API_KEY, or OPENAI_BASE_URL and OPENAI_API_KEY.
A plain request
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({ baseURL: "https://api.router.ezat.io", apiKey: process.env.EZAT_API_KEY });
const message = await client.messages.create({
model: "anthropic/claude-sonnet-5-5",
max_tokens: 64,
messages: [{ role: "user", content: "Say hello in five words." }],
});
for (const block of message.content) {
if (block.type === "text") console.log(block.text);
}
console.log(message.usage);
console.log("request-id:", message._request_id);usage has the token counts you are charged for; see Pricing.
Streaming
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({ baseURL: "https://api.router.ezat.io", apiKey: process.env.EZAT_API_KEY });
const stream = client.messages.stream({
model: "anthropic/claude-sonnet-5-5",
max_tokens: 64,
messages: [{ role: "user", content: "Count from one to five." }],
});
stream.on("text", (text) => process.stdout.write(text));
const message = await stream.finalMessage();
console.log("\n", message.usage);A tool call round trip
The model asks for a tool, your code runs it, and you send the result back with the conversation so far.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({ baseURL: "https://api.router.ezat.io", apiKey: process.env.EZAT_API_KEY });
const tools: Anthropic.Tool[] = [
{
name: "get_weather",
description: "Get the current weather in a city.",
input_schema: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
},
},
];
const messages: Anthropic.MessageParam[] = [
{ role: "user", content: "What is the weather in Paris? Use the get_weather tool." },
];
const first = await client.messages.create({ model: "anthropic/claude-sonnet-5-5", max_tokens: 64, tools, messages });
const call = first.content.find((block): block is Anthropic.ToolUseBlock => block.type === "tool_use");
if (!call) throw new Error(`No tool call (stop_reason: ${first.stop_reason})`);
console.log("Tool call:", call.name, call.input);
// Run the tool, then send its result back.
messages.push({ role: "assistant", content: first.content });
messages.push({
role: "user",
content: [{ type: "tool_result", tool_use_id: call.id, content: "Sunny and 18 degrees Celsius" }],
});
const second = await client.messages.create({ model: "anthropic/claude-sonnet-5-5", max_tokens: 64, tools, messages });
for (const block of second.content) {
if (block.type === "text") console.log(block.text);
}An image
Images go inline as base64. This one is an 8×8 red PNG; for a file of your own, use readFileSync("photo.png").toString("base64") from node:fs.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({ baseURL: "https://api.router.ezat.io", apiKey: process.env.EZAT_API_KEY });
const png =
"iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAAEUlEQVR42mP4z8CAFTEMLQkAKP8/wc53yE8AAAAASUVORK5CYII=";
const message = await client.messages.create({
model: "anthropic/claude-sonnet-5-5",
max_tokens: 64,
messages: [
{
role: "user",
content: [
{ type: "image", source: { type: "base64", media_type: "image/png", data: png } },
{ type: "text", text: "What colour is this image? Answer in one word." },
],
},
],
});
for (const block of message.content) {
if (block.type === "text") console.log(block.text);
}Count tokens
Anthropic SDK only. Counting is free, but it counts toward the key's requests per minute.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({ baseURL: "https://api.router.ezat.io", apiKey: process.env.EZAT_API_KEY });
const count = await client.messages.countTokens({
model: "anthropic/claude-sonnet-5-5",
messages: [{ role: "user", content: "Say hello in five words." }],
});
console.log(count.input_tokens);List models
These list the models your key can call. See Models for the public catalog with prices.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({ baseURL: "https://api.router.ezat.io", apiKey: process.env.EZAT_API_KEY });
for await (const model of client.models.list()) {
console.log(model.id, model.display_name);
}Handle errors
Each SDK throws its own error classes. The SDKs already retry connection errors, 429 and 5xx twice, so an error you catch has run out of retries (raise maxRetries on the client to try longer). This request asks for a model that doesn't exist, so it fails with 404.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({ baseURL: "https://api.router.ezat.io", apiKey: process.env.EZAT_API_KEY });
try {
await client.messages.create({
model: "anthropic/no-such-model",
max_tokens: 16,
messages: [{ role: "user", content: "Hi" }],
});
} catch (error) {
if (error instanceof Anthropic.APIConnectionError) {
console.error("Could not reach the API:", error.message);
process.exit(1);
}
if (!(error instanceof Anthropic.APIError)) throw error;
if (error instanceof Anthropic.RateLimitError) {
console.log("Rate limited. Retry after", error.headers?.get("retry-after"), "seconds");
}
console.log("status:", error.status, "type:", error.type);
// request-id is set once a request reaches a model; x-request-id is on every response.
console.log("request-id:", error.requestID ?? "none");
console.log("x-request-id:", error.headers?.get("x-request-id"));
}| Class | Status |
|---|---|
BadRequestError | 400 |
AuthenticationError | 401 |
APIError with status 402 | 402: add credit |
PermissionDeniedError | 403 |
NotFoundError | 404 |
RateLimitError | 429 |
InternalServerError | 500 and above, including 529 (Anthropic) and 503 (OpenAI) |
APIConnectionError | No response: network problem or timeout |
What each status means and whether to retry it is on Errors.
For AI agents
- Set
baseURLtohttps://api.router.ezat.iofor@anthropic-ai/sdk, orhttps://api.router.ezat.io/openai/v1foropenai, and pass the key fromprocess.env.EZAT_API_KEY. - Use model ids from
GET https://api.router.ezat.io/v1/models; don't invent them. max_tokensis required with the Anthropic SDK.- Don't add your own retry loop on top of the SDK's for 429 and 5xx unless you need more than its two retries; raise
maxRetriesinstead. - Log
request-id(orx-request-id) from errors so the user can give it to support@ezat.io.