# Node.js

URL: https://docs.router.ezat.io/examples/node

> 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](https://docs.router.ezat.io/examples/curl.md).

## Set up

Install the SDK you want (or both), and put your key in an environment variable:

```bash
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

```ts tab="Anthropic SDK" test
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);
```

```ts tab="OpenAI SDK" test
import OpenAI from "openai";

const client = new OpenAI({ baseURL: "https://api.router.ezat.io/openai/v1", apiKey: process.env.EZAT_API_KEY });

const completion = await client.chat.completions.create({
  model: "anthropic/claude-sonnet-5-5",
  max_completion_tokens: 64,
  messages: [{ role: "user", content: "Say hello in five words." }],
});

console.log(completion.choices[0].message.content);
console.log(completion.usage);
console.log("x-request-id:", completion._request_id);
```

`usage` has the token counts you are charged for; see [Pricing](https://docs.router.ezat.io/pricing.md).

## Streaming

```ts tab="Anthropic SDK" test
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);
```

```ts tab="OpenAI SDK" test
import OpenAI from "openai";

const client = new OpenAI({ baseURL: "https://api.router.ezat.io/openai/v1", apiKey: process.env.EZAT_API_KEY });

const stream = await client.chat.completions.create({
  model: "anthropic/claude-sonnet-5-5",
  max_completion_tokens: 64,
  stream: true,
  stream_options: { include_usage: true },
  messages: [{ role: "user", content: "Count from one to five." }],
});

for await (const chunk of stream) {
  const text = chunk.choices[0]?.delta?.content;
  if (text) process.stdout.write(text);
  // With include_usage, the last chunk has the usage and no choices.
  if (chunk.usage) console.log("\n", chunk.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.

```ts tab="Anthropic SDK" test
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);
}
```

```ts tab="OpenAI SDK" test
import OpenAI from "openai";

const client = new OpenAI({ baseURL: "https://api.router.ezat.io/openai/v1", apiKey: process.env.EZAT_API_KEY });

const tools: OpenAI.ChatCompletionTool[] = [
  {
    type: "function",
    function: {
      name: "get_weather",
      description: "Get the current weather in a city.",
      parameters: {
        type: "object",
        properties: { city: { type: "string" } },
        required: ["city"],
      },
    },
  },
];
const messages: OpenAI.ChatCompletionMessageParam[] = [
  { role: "user", content: "What is the weather in Paris? Use the get_weather tool." },
];

const first = await client.chat.completions.create({
  model: "anthropic/claude-sonnet-5-5",
  max_completion_tokens: 64,
  tools,
  messages,
});
const reply = first.choices[0].message;
const call = reply.tool_calls?.[0];
if (call?.type !== "function") {
  throw new Error(`No tool call (finish_reason: ${first.choices[0].finish_reason})`);
}
console.log("Tool call:", call.function.name, call.function.arguments);

// Run the tool, then send its result back.
messages.push(reply);
messages.push({ role: "tool", tool_call_id: call.id, content: "Sunny and 18 degrees Celsius" });

const second = await client.chat.completions.create({
  model: "anthropic/claude-sonnet-5-5",
  max_completion_tokens: 64,
  tools,
  messages,
});
console.log(second.choices[0].message.content);
```

## 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`.

```ts tab="Anthropic SDK" test
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);
}
```

```ts tab="OpenAI SDK" test
import OpenAI from "openai";

const client = new OpenAI({ baseURL: "https://api.router.ezat.io/openai/v1", apiKey: process.env.EZAT_API_KEY });

const png =
  "iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAAEUlEQVR42mP4z8CAFTEMLQkAKP8/wc53yE8AAAAASUVORK5CYII=";

const completion = await client.chat.completions.create({
  model: "anthropic/claude-sonnet-5-5",
  max_completion_tokens: 64,
  messages: [
    {
      role: "user",
      content: [
        { type: "image_url", image_url: { url: `data:image/png;base64,${png}` } },
        { type: "text", text: "What colour is this image? Answer in one word." },
      ],
    },
  ],
});

console.log(completion.choices[0].message.content);
```

## Count tokens

Anthropic SDK only. Counting is free, but it counts toward the key's requests per minute.

```ts test
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](https://docs.router.ezat.io/models.md) for the public catalog with prices.

```ts tab="Anthropic SDK" test
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);
}
```

```ts tab="OpenAI SDK" test
import OpenAI from "openai";

const client = new OpenAI({ baseURL: "https://api.router.ezat.io/openai/v1", apiKey: process.env.EZAT_API_KEY });

for await (const model of client.models.list()) {
  console.log(model.id, model.owned_by);
}
```

## 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.

```ts tab="Anthropic SDK" test
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"));
}
```

```ts tab="OpenAI SDK" test
import OpenAI from "openai";

const client = new OpenAI({ baseURL: "https://api.router.ezat.io/openai/v1", apiKey: process.env.EZAT_API_KEY });

try {
  await client.chat.completions.create({
    model: "anthropic/no-such-model",
    max_completion_tokens: 16,
    messages: [{ role: "user", content: "Hi" }],
  });
} catch (error) {
  if (error instanceof OpenAI.APIConnectionError) {
    console.error("Could not reach the API:", error.message);
    process.exit(1);
  }
  if (!(error instanceof OpenAI.APIError)) throw error;
  if (error instanceof OpenAI.RateLimitError) {
    console.log("Rate limited. Retry after", error.headers?.get("retry-after"), "seconds");
  }
  console.log("status:", error.status, "code:", error.code);
  console.log("x-request-id:", error.requestID);
  console.log("request-id:", error.headers?.get("request-id") ?? "none");
}
```

| 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](https://docs.router.ezat.io/reference/errors.md).

## For AI agents

1. Set `baseURL` to `https://api.router.ezat.io` for `@anthropic-ai/sdk`, or `https://api.router.ezat.io/openai/v1` for `openai`, and pass the key from `process.env.EZAT_API_KEY`.
2. Use model ids from `GET https://api.router.ezat.io/v1/models`; don't invent them.
3. `max_tokens` is required with the Anthropic SDK.
4. 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 `maxRetries` instead.
5. Log `request-id` (or `x-request-id`) from errors so the user can give it to support@ezat.io.