# Node.js

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

> 使用 Anthropic 和 OpenAI SDK 的 Node.js 示例：普通请求、流式输出、工具调用往返、图片、计算 token 数、列出模型和处理错误。

## 何时阅读本页

你在 Node.js 或 TypeScript 中通过官方 Anthropic SDK（`@anthropic-ai/sdk`）或 OpenAI SDK（`openai`）调用 EZAT Router。原始 HTTP 请求见 [curl](https://docs.router.ezat.io/zh/examples/curl.md)。

## 安装与配置

安装你要用的 SDK（或者两个都装），并把 key 放进环境变量：

```bash
npm install @anthropic-ai/sdk openai
export EZAT_API_KEY="ezat_sk_…"
```

示例是使用顶层 `await` 的 TypeScript ES 模块。把一个示例保存为 `example.mts`，然后运行 `node example.mts`（Node.js 22.18 及以上版本可以直接运行 TypeScript）；也可以去掉类型注解，保存为 `example.mjs`。

每个 SDK 只需要改 base URL：

| SDK | `baseURL` | 调用的端点 |
| --- | --- | --- |
| `@anthropic-ai/sdk` | `https://api.router.ezat.io` | `/v1/messages` |
| `openai` | `https://api.router.ezat.io/openai/v1` | `/openai/v1/chat/completions` |

两个 SDK 也会从环境变量读取 base URL 和 key，对于不是你自己编写的工具很方便：`ANTHROPIC_BASE_URL` 和 `ANTHROPIC_API_KEY`，或 `OPENAI_BASE_URL` 和 `OPENAI_API_KEY`。

## 普通请求

```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` 里是计费所依据的 token 数；见[价格](https://docs.router.ezat.io/zh/pricing.md)。

## 流式输出

```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);
}
```

## 工具调用往返

模型请求调用工具，你的代码运行工具，再把结果连同目前为止的对话一起发回。

```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);
```

## 图片

图片以 base64 内嵌发送。这里是一张 8×8 的红色 PNG；如果要用你自己的文件，可以用 `node:fs` 中的 `readFileSync("photo.png").toString("base64")`。

```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);
```

## 计算 token 数

仅限 Anthropic SDK。计算 token 数免费，但会计入 key 的每分钟请求数。

```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);
```

## 列出模型

下面的代码列出你的 key 能调用的模型。带价格的公开模型目录见[模型](https://docs.router.ezat.io/zh/models.md)。

```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);
}
```

## 处理错误

每个 SDK 都会抛出自己的错误类。SDK 已经会对连接错误、429 和 5xx 重试两次，所以你捕获到的错误已经用完了重试次数（如需重试更久，请在客户端上调高 `maxRetries`）。这个请求使用了一个不存在的模型，所以会以 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");
}
```

| 类 | 状态码 |
| --- | --- |
| `BadRequestError` | 400 |
| `AuthenticationError` | 401 |
| `status` 为 402 的 `APIError` | 402：需要充值 |
| `PermissionDeniedError` | 403 |
| `NotFoundError` | 404 |
| `RateLimitError` | 429 |
| `InternalServerError` | 500 及以上，包括 529（Anthropic）和 503（OpenAI） |
| `APIConnectionError` | 没有响应：网络问题或超时 |

每个状态码的含义以及是否应该重试，见[错误](https://docs.router.ezat.io/zh/reference/errors.md)。

## 给 AI agent 的说明

1. 对 `@anthropic-ai/sdk` 把 `baseURL` 设为 `https://api.router.ezat.io`，对 `openai` 设为 `https://api.router.ezat.io/openai/v1`，并从 `process.env.EZAT_API_KEY` 传入 key。
2. 使用 `GET https://api.router.ezat.io/v1/models` 返回的模型 id；不要自己编造。
3. 使用 Anthropic SDK 时，`max_tokens` 为必填项。
4. 除非需要超过两次重试，否则不要在 SDK 之上为 429 和 5xx 再加一层自己的重试循环；改为调高 `maxRetries`。
5. 记录错误中的 `request-id`（或 `x-request-id`），方便用户提供给 support@ezat.io。