← 返回博客

Node.js 调用 Claude、GPT、Gemini API 教程

Zivv••22 分钟阅读
Node.jsTypeScriptSDK

前端和全栈开发者接入大模型,最常见的技术栈就是 Node.js 或 TypeScript:一个 Express / Next.js 后端,把用户的输入转给模型,再把结果流式推回浏览器。官方文档通常只讲自家模型,真正落地时你会同时碰到三件事:用哪个 SDK、怎么流式、怎么让长请求不超时。这篇用一个 Key 把 Claude、GPT、Gemini 三家模型都跑通,从最小可运行代码一路讲到 Next.js 路由转发流和生产级的超时重试。

如果你还不清楚「中转站」是什么、为什么一个 Key 能调多家模型,先看 AI API 中转站是什么;如果你已有一套基于官方 SDK 的代码想迁移过来,可以配合 OpenAI 兼容接口迁移指南 一起看。

准备工作:Key、Base URL 与模型名

开始写代码前确认三样东西:

  • API Key:在 Zivv 控制台创建,形如 sk-...,建议放在环境变量里,不要写进代码仓库
  • Base URL:取决于你用哪种协议调用
  • 模型 ID:以 模型广场 或 GET /v1/models 返回的为准,本文示例用 claude-sonnet-5、gpt-5.5、gemini-3.5-flash
协议Base URLNode.js 推荐 SDK适用场景
OpenAI 兼容https://zivv.pro/v1openai一套代码调用所有模型,最省事
Anthropic 原生https://zivv.pro@anthropic-ai/sdk需要 Claude 原生能力(thinking、原生工具调用格式等)
Gemini 兼容https://zivv.pro/v1betaGoogle GenAI SDK已有 Gemini 原生代码

对大多数项目,建议的组合是:默认用 openai SDK 调用所有模型,只有确实需要 Claude 原生特性的模块再引入 @anthropic-ai/sdk。这样业务代码只有一种消息格式,切模型只改一个字符串。

安装依赖(Node.js 18 及以上,自带 fetch 与 Web Streams):

npm install openai @anthropic-ai/sdk
# 本地开发读取 .env
npm install dotenv

在项目根目录的 .env 中写入:

ZIVV_API_KEY=sk-your-zivv-key

第一步:用 openai SDK 调用三家模型

openai 官方 Node SDK 支持自定义 baseURL,指向 Zivv 后就能用同一个客户端调用 Claude、GPT 和 Gemini:

import "dotenv/config";
import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.ZIVV_API_KEY,
  baseURL: "https://zivv.pro/v1",
});

async function ask(model: string, question: string) {
  const res = await client.chat.completions.create({
    model,
    messages: [
      { role: "system", content: "你是一个简洁的技术助手。" },
      { role: "user", content: question },
    ],
  });
  return res.choices[0].message.content;
}

for (const model of ["claude-sonnet-5", "gpt-5.5", "gemini-3.5-flash"]) {
  console.log(model, "=>", await ask(model, "用一句话解释什么是事件循环"));
}

几点说明:

  • 文件扩展名用 .mts 或在 package.json 里设置 "type": "module",才能使用顶层 await
  • 响应结构和官方 OpenAI 完全一致,res.usage 里有本次的输入输出 Token,可以直接拿来做成本统计
  • 换模型只需要换 model 字段,消息格式、返回结构都不变

第二步:用 @anthropic-ai/sdk 调用 Claude 原生接口

如果你要用 Claude 的原生消息格式(比如 system 作为顶层字段、content block 结构、原生工具调用),就用 Anthropic SDK。注意 Base URL 不带 /v1,SDK 会自己拼接 /v1/messages:

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

const anthropic = new Anthropic({
  apiKey: process.env.ZIVV_API_KEY,
  baseURL: "https://zivv.pro",
});

const msg = await anthropic.messages.create({
  model: "claude-sonnet-5",
  max_tokens: 1024,
  system: "你是一个资深 Node.js 工程师。",
  messages: [{ role: "user", content: "Express 和 Fastify 怎么选?" }],
});

for (const block of msg.content) {
  if (block.type === "text") console.log(block.text);
}
console.log(msg.usage); // input_tokens / output_tokens

和 OpenAI 格式相比要注意两个差异:Anthropic 协议的 max_tokens 是必填的;返回的 content 是数组,文本要从 type === "text" 的块里取。在发送超长上下文前,如果想先估算成本,可以调用 /v1/messages/count_tokens 预先计数,再配合 Token 成本计算指南 算出费用。

第三步:流式输出

聊天类产品几乎都需要流式:首字更快出现,长输出也不容易触发读取超时。两个 SDK 的写法:

// openai SDK:stream: true 后得到一个异步可迭代对象
const stream = await client.chat.completions.create({
  model: "gpt-5.5",
  messages: [{ role: "user", content: "写一段 Node.js 读取大文件的示例" }],
  stream: true,
});

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

// @anthropic-ai/sdk:messages.stream 提供事件回调和最终消息
const claudeStream = anthropic.messages.stream({
  model: "claude-sonnet-5",
  max_tokens: 2048,
  messages: [{ role: "user", content: "解释 Node.js 的 Stream 背压机制" }],
});

claudeStream.on("text", (text) => process.stdout.write(text));
const finalMsg = await claudeStream.finalMessage();
console.log("\n用量:", finalMsg.usage);

注意 openai 流里取内容要用可选链 ?.:部分 chunk 的 choices 可能为空或 delta 里没有 content(比如角色声明、结束块),直接取属性会抛异常。流式背后的 SSE 事件格式、断流判断和 nginx 缓冲问题比较多,单独写在 AI API 流式输出 SSE 实现与常见坑 里。

第四步:在 Next.js 路由里转发流

最常见的生产架构是:浏览器 → 你的 Next.js 后端 → Zivv。Key 只放在服务端,浏览器永远拿不到。App Router 下的路由处理函数可以直接返回一个 ReadableStream:

// app/api/chat/route.ts
import OpenAI from "openai";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";

const client = new OpenAI({
  apiKey: process.env.ZIVV_API_KEY,
  baseURL: "https://zivv.pro/v1",
});

export async function POST(req: Request) {
  const { messages, model = "claude-sonnet-5" } = await req.json();

  const upstream = await client.chat.completions.create(
    { model, messages, stream: true },
    { signal: req.signal } // 用户关闭页面时同步取消上游请求
  );

  const encoder = new TextEncoder();
  const body = new ReadableStream({
    async start(controller) {
      try {
        for await (const chunk of upstream) {
          const text = chunk.choices[0]?.delta?.content;
          if (text) controller.enqueue(encoder.encode(text));
        }
        controller.close();
      } catch (err) {
        controller.error(err);
      }
    },
  });

  return new Response(body, {
    headers: {
      "Content-Type": "text/plain; charset=utf-8",
      "Cache-Control": "no-cache, no-transform",
      "X-Accel-Buffering": "no",
    },
  });
}

这段代码有几个容易漏掉的细节:

  • 只把纯文本转给前端:上游是 SSE,转发时提取出文本再推给浏览器,前端解析最简单;如果需要传工具调用、用量等结构化信息,可以改成自己定义的 SSE 或 NDJSON 格式
  • 传入 `req.signal`:用户点「停止」或关掉页面,上游请求随之取消,不再继续生成,避免白白消耗 Token
  • `X-Accel-Buffering: no`:告诉前面的 nginx 不要缓冲这条响应,否则用户会看到「卡住几秒后一次性全部出现」
  • 白名单校验 model:生产环境不要让前端随意指定模型名,在服务端用白名单限制可选模型,防止被滥用到昂贵模型上

前端用 fetch 读取这个流:

const res = await fetch("/api/chat", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ messages }),
});

const reader = res.body!.getReader();
const decoder = new TextDecoder();
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  appendToUI(decoder.decode(value, { stream: true }));
}

decode 时的 { stream: true } 很重要:一个中文字符在 UTF-8 里占 3 个字节,可能被切在两个数据块之间,不加这个参数会出现乱码。

第五步:超时与重试

两个 SDK 都内置了超时和自动重试,但默认值不一定适合你的场景:

配置openai SDK@anthropic-ai/sdk建议
超时timeout(毫秒)timeout(毫秒)非流式长输出至少 300000;流式可适当缩短
自动重试maxRetries(默认 2)maxRetries(默认 2)2 到 4 次,SDK 自带指数退避
单次覆盖第二个参数传 { timeout, maxRetries }同左批处理任务单独放宽
取消signalsignal绑定用户请求或任务生命周期
const client = new OpenAI({
  apiKey: process.env.ZIVV_API_KEY,
  baseURL: "https://zivv.pro/v1",
  timeout: 300_000, // 5 分钟
  maxRetries: 3,
});

// 某个批处理请求单独放宽
await client.chat.completions.create(
  { model: "claude-opus-4-8", messages, max_tokens: 16000 },
  { timeout: 600_000, maxRetries: 4 }
);

SDK 的自动重试会处理连接错误、408、409、429 和 5xx 等可重试错误。需要注意:

  • 400、401、402、403、404 不该重试:这些是请求本身的问题,重试只会重复失败。对应排查见 401 Key 无效、402 余额不足、404 模型不存在
  • 529 过载需要更长的退避:高峰期上游过载可能持续几分钟,默认重试次数可能不够,详见 529 与超时排查
  • 流式请求重试要小心:已经推给用户一半内容再重试,会出现重复文本。更稳妥的做法是只在「还没收到任何数据」时自动重试,中途断开则提示用户重新生成
  • 部署平台也有超时:Serverless 函数通常有执行时长上限,长输出任务要确认平台配置允许足够的时长,否则函数会在 SDK 超时之前被平台掐断

第六步:一个可复用的封装

实际项目里建议把模型调用收敛到一个模块,业务代码只关心「问什么、用哪个档位」:

// lib/llm.ts
import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.ZIVV_API_KEY,
  baseURL: "https://zivv.pro/v1",
  timeout: 300_000,
  maxRetries: 3,
});

const TIERS = {
  fast: "gemini-3.5-flash",
  default: "claude-sonnet-5",
  strong: "claude-opus-4-8",
} as const;

export async function chat(
  tier: keyof typeof TIERS,
  messages: OpenAI.Chat.ChatCompletionMessageParam[]
) {
  const res = await client.chat.completions.create({
    model: TIERS[tier],
    messages,
  });
  return { text: res.choices[0].message.content ?? "", usage: res.usage };
}

档位映射集中在一处,换模型、做 A/B 对比、按任务难度分流都只改这里。更系统的分层思路见 多模型路由策略。

生产环境的几条安全习惯

代码跑通只是第一步,上线前建议对照检查一遍:

  • 按环境拆分 Key:开发、测试、生产各用一把 Key,开发 Key 泄露不影响线上;某个环境出现异常用量,也能一眼定位来源
  • Key 只存在于服务端环境变量:部署平台的环境变量面板或密钥管理服务里配置,不要提交 .env 文件,在 .gitignore 里显式排除
  • 给用户请求加限流:你的 /api/chat 是公开接口,没有登录校验和频率限制,就可能被脚本刷爆余额。至少按用户或 IP 做简单的每分钟次数限制
  • 限制 max_tokens 和上下文长度:在服务端裁剪过长的历史消息、设置合理的输出上限,单次请求的成本就有了上界
  • 记录用量而不是记录正文:日志里记模型名、Token 数、耗时和状态码就够排查问题,避免把用户对话全文写进日志。Zivv 自身也只记录 Token 用量用于计费,不存储请求和响应正文

团队协作时,可以给每个服务或成员单独发 Key,并设置日、月预算上限,在 团队模式 里按成员、Key、模型查看用量,比所有人共用一把主 Key 安全得多。

常见报错速查

现象常见原因处理
404 Not FoundBase URL 拼错,比如 Anthropic SDK 写成了 https://zivv.pro/v1Anthropic SDK 用 https://zivv.pro,openai SDK 用 https://zivv.pro/v1
model not found模型 ID 拼写或大小写不对以 GET /v1/models 返回为准
401环境变量没读到,apiKey 为 undefined检查 dotenv 是否在最先加载、部署平台是否配置了变量
流式一次性全部出现nginx 或 CDN 缓冲了响应关闭代理缓冲,加 X-Accel-Buffering: no
中文乱码TextDecoder 未使用 stream 模式decode(value, { stream: true })

常见问题 FAQ

Q:Node.js 里调用 Claude,用 openai SDK 还是 @anthropic-ai/sdk? A:只做普通对话、想一套代码调所有模型,用 openai SDK 指向 https://zivv.pro/v1 最省事;需要 Claude 原生的消息结构、thinking 或原生工具调用格式,用 @anthropic-ai/sdk 指向 https://zivv.pro。两者用同一个 Key。

Q:TypeScript 项目里怎么获得类型提示? A:两个官方 SDK 都自带完整的类型定义,安装后直接可用,比如 OpenAI.Chat.ChatCompletionMessageParam 可以用来约束消息数组,不需要额外安装类型包。

Q:能在浏览器前端直接调用 API 吗? A:不建议。把 Key 放进前端代码等于公开它,任何人都能拿去消耗你的余额。正确做法是像本文的 Next.js 路由一样,由服务端持有 Key 并转发。团队项目还可以给每个应用单独建 Key 并设置预算,见 团队 Key 与预算管理。

Q:可以用这些 SDK 生成向量(embeddings)吗? A:Zivv 目前不提供 embeddings 接口,主要覆盖对话、Responses、图像生成等端点(完整列表见 端点参考)。做 RAG 时,向量部分请使用其他向量服务,对话生成部分再走 Zivv。

Q:流式和非流式的计费一样吗? A:一样,都按实际消耗的输入输出 Token 计费,价格为分组倍率乘以官方模型单价。流式只是改变返回方式,不改变 Token 数量。

Q:Node.js 最低需要什么版本? A:建议 Node.js 18 及以上,原生支持 fetch、ReadableStream 和 TextDecoder,SDK 与本文的 Next.js 示例都依赖这些能力。

下一步

  1. 注册 Zivv 并在控制台创建一个开发用 Key
  2. 复制第一步的代码,确认三家模型都能返回结果
  3. 把 Next.js 路由和前端读流代码接进你的项目
  4. 按业务需要调整超时和重试,再用 模型广场 挑选合适的模型档位

更多 SDK 细节可以查阅 OpenAI SDK 接入文档。