安装与最小调用
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,避免默认导出被解析成命名空间。
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 好排查。
流式输出
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 版同样支持。不加就只能按字符数粗估费用,月底对账会差一截。
错误处理与重试
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 跑一遍上面的示例,确认能出字再接进项目。