首页 / 文章 / Node.js 调用 OpenAI 兼容接口

Node.js 调用 OpenAI 兼容接口

用 openai npm 包接二五云API:baseURL 怎么填、TypeScript 流式读取、AbortSignal 取消、常见异常分类,以及 glm-5.3 每次请求的成本估算。

安装与最小调用

npm install openai 即可。包内部用 fetch 发请求,Node 18 及以上可以直接跑,不需要额外装 polyfill;TypeScript 项目自带类型定义,不用再装 @types/openai。项目用 ESM 就写 import OpenAI from "openai",CommonJS 用 const OpenAI = require("openai"),两种都支持。用 tsx 或 ts-node 直接跑 TS 时,把 module 设成 NodeNext,避免默认导出被解析成命名空间。

ts
import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.YUN25_API_KEY,
  baseURL: "https://ai.idcidiot.com/v1",
  timeout: 60_000,
  maxRetries: 2,
});

const resp = await client.chat.completions.create({
  model: "glm-5.3",
  messages: [{ role: "user", content: "把这段 SQL 改写成 GORM 查询" }],
});
console.log(resp.choices[0]?.message?.content);

Node 版的字段名是驼峰 baseURL,和 Python SDK 的 base_url 不一样,照抄 Python 示例会静默走默认地址,表现为请求确实发出去了,但账单和日志里都没有记录。timeout 单位是毫秒,作用在单次请求上;apiKey 没读到时会抛一个明确的初始化错误,比 401 好排查。

流式输出

ts
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 120_000);

const stream = await client.chat.completions.create(
  {
    model: "glm-5.3",
    messages: [{ role: "user", content: "逐条列出 HTTP 缓存头的含义" }],
    stream: true,
  },
  { signal: controller.signal },
);

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

for await 会一直读到流结束,SDK 已经处理好 SSE 分帧和 [DONE],不用自己切 data: 行。中途 abort 会抛一个取消类异常,按业务取消处理即可,不要当成服务端错误重试。

要拿完整文本,在循环里把 delta.content 追加到数组,结束后 join("")。只写 process.stdout.write 不保存,函数返回时手里只有一堆已经打印出去的片段。finish_reason 出现在末尾一块,值是 length 表示被 max_tokens 截断。

流式请求默认不返回 usage,要像 Python 那样加 stream_options: { include_usage: true },Node 版同样支持。不加就只能按字符数粗估费用,月底对账会差一截。

错误处理与重试

ts
import { APIConnectionError, APIError, RateLimitError } from "openai";

try {
  await client.chat.completions.create({
    model: "glm-5.3",
    messages: [{ role: "user", content: "ping" }],
  });
} catch (err) {
  if (err instanceof RateLimitError) {
    console.warn("限流,retry-after:", err.headers?.get("retry-after"));
  } else if (err instanceof APIConnectionError) {
    console.error("连不上,检查网络或 baseURL");
  } else if (err instanceof APIError) {
    console.error(err.status, err.message);
  } else {
    throw err;
  }
}

RateLimitError 继承自 APIError,判断顺序要从具体到宽泛。maxRetries 默认 2,遇到 429 和 5xx 会退避重试;headers.get("retry-after") 能拿到服务端建议的等待秒数,有值就按它等。APIError 上还有 request_id,报问题时把它一起发给客服,对方能直接定位日志。

别把所有异常都吞掉。网络错误重试有意义,400 和 401 重试多少次都一样,只会拖慢响应。按状态码分流,日志里留下 status、message 和请求参数,排查会快很多。

如果错误信息里是 HTML 或者代理的错误页,说明请求没到网关,先检查 baseURL 的域名和协议;网关自己返回的错误一般是 JSON,带 message 和 type 字段。

模型与并发

一个 Key 能开多少并发取决于分组,不是本地限制的。用 Promise.all 一次发 20 个请求,很可能前几个成功、后面全 429;给并发加个上限,比如 5 到 10,再用重试兜住偶发的限流。批处理宁可慢一点,也不要一半失败后整体重跑。

切模型只改 model 字符串。高频短输出用 glm-5.3-flash,长上下文和工具调用用 glm-5.3,要读图换 gemini-3.8-flash。先用小样本验证格式,再放大批量。

超时与取消

timeout 管的是单次尝试,重试会把总耗时拉长,长文生成时把它设到 120 秒以上更稳。用户关页面、任务被取消这类场景用 AbortSignal,它比超时更及时,也能省掉一次已经没人要的生成。

在 Web 服务里,把请求的 req.signal 透传给 SDK,客户端断开时上游请求会一起取消。Node 的 AbortSignal.timeout(ms) 可以直接生成一个带超时的信号,省掉手动 setTimeout。两个信号可以用 AbortSignal.any([...]) 合并,任一触发都会取消。重试和取消同时存在时,先取消再重试,否则旧请求的结果可能晚到,把新结果覆盖掉。

成本

glm-5.3 的 model_ratio 是 4,稳定国模分组 0.2 倍,输入价 = 4 × 2 × 0.2 = $1.6/百万 token,输出按 completion_ratio 3.5 折算为 $5.6/百万 token;上下文 1,000,000 token,最大输出 131,072 token。一天 2,000 次请求、每次 1,500 输入 400 输出,就是 3M 输入加 0.8M 输出,约 $4.8 + $4.48 = $9.3。

同类任务换成 gemini-3.8-flash(Gemini Antigravity 0.65 倍,$0.975/$4.875)价格差不多,但多模态输入更强。把模型名和采样参数抽成配置对象,换模型时只改一处。倍率在 价格页,单模型页在 /model/glm-5.3/。先用 tsx 跑一遍上面的示例,确认能出字再接进项目。

用 二五云API 跑通这些模型

OpenAI 兼容接口,一个 Key 调用 GPT、Claude、Gemini、DeepSeek、Kimi 等模型,按量计费。

免费注册查看模型价格

相关文章