SDK Node.js
استفاده از پکیج رسمی openai در Node.js و TypeScript با یوتاپن؛ نصب، کلاینت، chat با هدرهای هزینه، استریم و AbortController، tool calling، embeddings.
بهروزرسانی: ۱۶ شهریور ۱۴۰۵
پکیج رسمی openai برای Node (نسخهٔ ۴ به بالا؛ نمونهها با ۷ تست شده) بدون تغییر با یوتاپن کار میکند. در مرورگر استفاده نکن؛ کلید لو میرود. برای فرانتاند، یک route در سرور خودت بگذار (نمونهٔ Next.js).
نصب و کلاینت
npm install openai
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://api.uttapen.ir/v1",
apiKey: process.env.UTTAPEN_API_KEY, // sk-up-...
});
گزینههای مفید: timeout (میلیثانیه)، maxRetries (پیشفرض ۲)، defaultHeaders برای X-Uttapen-Include-Meta. متغیرهای محیطی OPENAI_BASE_URL و OPENAI_API_KEY هم خوانده میشوند.
Chat
const resp = await client.chat.completions.create({
model: "openai/gpt-5-mini",
messages: [
{ role: "system", content: "پاسخ کوتاه و فارسی بده." },
{ role: "user", content: "تفاوت map و forEach در JavaScript؟" },
],
max_tokens: 300,
});
console.log(resp.choices[0].message.content);
console.log(resp.usage?.prompt_tokens, resp.usage?.completion_tokens);
برای هدرهای هزینه از .withResponse() استفاده کن:
const { data, response } = await client.chat.completions
.create({ model: "openai/gpt-5-mini", messages: [{ role: "user", content: "سلام" }] })
.withResponse();
console.log(response.headers.get("x-uttapen-cost-toman"), response.headers.get("x-uttapen-balance-toman"));
console.log(data.choices[0].message.content);
استریم
const controller = new AbortController();
const stream = await client.chat.completions.create(
{
model: "openai/gpt-5-mini",
messages: [{ role: "user", content: "یک تابع اعتبارسنجی شماره موبایل ایران بنویس." }],
stream: true,
},
{ headers: { "X-Uttapen-Include-Meta": "1" }, signal: controller.signal },
);
for await (const chunk of stream) {
if (chunk.object === "uttapen.meta") {
console.log(`\n[هزینه: ${(chunk as any).cost_toman} تومان]`);
continue;
}
process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}
controller.abort() استریم را از سمت تو قطع میکند؛ gateway درخواست upstream را لغو و فقط مصرف تا آن لحظه را تسویه میکند. در TypeScript فیلدهای uttapen.meta در تایپ ChatCompletionChunk نیستند، برای همین cast لازم است.
Tool calling
const tools: OpenAI.Chat.ChatCompletionTool[] = [{
type: "function",
function: {
name: "get_weather",
description: "دمای فعلی یک شهر",
parameters: { type: "object", properties: { city: { type: "string" } }, required: ["city"] },
},
}];
const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [{ role: "user", content: "هوای تهران؟" }];
const r1 = await client.chat.completions.create({ model: "openai/gpt-5-mini", messages, tools });
const msg = r1.choices[0].message;
messages.push(msg);
for (const call of msg.tool_calls ?? []) {
if (call.type !== "function") continue;
const args = JSON.parse(call.function.arguments);
messages.push({ role: "tool", tool_call_id: call.id, content: JSON.stringify({ city: args.city, temp_c: 31 }) });
}
const r2 = await client.chat.completions.create({ model: "openai/gpt-5-mini", messages, tools });
console.log(r2.choices[0].message.content);
اگر حلقهٔ خودکار میخواهی، client.chat.completions.runTools(...) در SDK همین کار را با تابعهای JavaScript میکند. جزئیات و استریم tool_calls در Tool calling.
Embeddings
const emb = await client.embeddings.create({
model: "openai/text-embedding-3-small",
input: ["قرارداد اجاره", "اجارهنامهٔ ملک مسکونی"],
});
console.log(emb.data.length, emb.data[0].embedding.length, emb.usage.prompt_tokens);
خروجی ساختیافته با Zod
import { z } from "zod";
import { zodResponseFormat } from "openai/helpers/zod";
const Ticket = z.object({ title: z.string(), priority: z.enum(["low", "medium", "high"]) });
const parsed = await client.chat.completions.parse({
model: "openai/gpt-5-mini",
messages: [{ role: "user", content: "سایت از دیشب بالا نمیآید، مشتریها شاکیاند." }],
response_format: zodResponseFormat(Ticket, "ticket"),
});
console.log(parsed.choices[0].message.parsed);
مدیریت خطا
try {
await client.chat.completions.create({ model: "openai/gpt-5-mini", messages });
} catch (err) {
if (err instanceof OpenAI.AuthenticationError) {
console.error("کلید uttapen نامعتبر است");
} else if (err instanceof OpenAI.RateLimitError) {
const wait = Number(err.headers?.get("retry-after") ?? 5);
await new Promise((r) => setTimeout(r, wait * 1000));
} else if (err instanceof OpenAI.APIError) {
if (err.status === 402) {
const info = (err.error as any)?.uttapen;
console.error("شارژ لازم:", info?.required_toman, "تومان", info?.topup_url);
} else {
console.error(err.status, err.code, err.message);
}
} else throw err;
}
err.code همان error.code ما و err.error کل بدنهٔ خطاست. err.headers.get("x-uttapen-request-id") شناسهٔ درخواست است (در نسخهٔ ۴ SDK، headers آبجکت ساده بود). کلاسها: BadRequestError، AuthenticationError، PermissionDeniedError، NotFoundError، RateLimitError، InternalServerError، APIConnectionError؛ برای 402 کلاس عمومی APIError. جدول کامل در خطاها.
فیلدهای اضافی
SDK فیلدهای ناشناخته را عبور میدهد؛ در TypeScript فقط cast لازم است:
await client.chat.completions.create({
model: "deepseek/deepseek-r1",
messages,
reasoning: { effort: "low" },
models: ["openai/o3-mini"],
} as any);
استفاده در سرور (Express، Next.js، Bun)
کلاینت را در یک ماژول بساز و همهجا import کن؛ SDK روی fetch استاندارد کار میکند و در Node ۱۸ به بالا، Bun، Deno و Edge Runtime نکست بدون polyfill اجرا میشود. در Next.js، فراخوانی فقط در Route Handler یا Server Action باشد؛ NEXT_PUBLIC_ روی نام متغیر کلید نگذار وگرنه در باندل مرورگر میرود.
// lib/uttapen.ts
import OpenAI from "openai";
export const uttapen = new OpenAI({
baseURL: "https://api.uttapen.ir/v1",
apiKey: process.env.UTTAPEN_API_KEY,
timeout: 120_000,
maxRetries: 1,
});
برای پاسخهای طولانی، استریم را مستقیم به کلاینت رله کن (نمونهٔ Route Handler) یا کار را به یک صف (BullMQ) بده و نتیجه را با X-Uttapen-Request-Id ذخیره کن. در Express، res.flushHeaders() و نوشتن هر چانک با res.write کافی است؛ فشردگی (compression) را برای مسیر استریم خاموش کن چون بافر میکند.
اشتباههای رایج
OPENAI_API_KEYقدیمی. اگرapiKeyرا صریح ندهی، SDK متغیر محیطی OpenAI را برمیدارد و401میگیری.Number()روی مبالغ.cost_tomanوbalance_tomanرشتهاند؛ برای جمع کردن ازdecimal.jsاستفاده کن یا در دیتابیسNUMERICنگه دار.- حلقهٔ
for awaitبدونtry. خطای وسط استریم (502) بهصورت exception از حلقه بیرون میآید؛ آن را بگیر و پیام کاربر را با متن ناقص ذخیره نکن. JSON.parseروی آرگومان ابزار بدونtry. مدلها گاهی JSON ناقص میدهند؛ خطا را بهعنوان پیامtoolبرگردان تا مدل اصلاح کند.- timeout پیشفرض ۱۰ دقیقه. برای درخواستهای کوتاه
timeoutرا کم کن تا یک اتصال گیرکرده worker را نگه ندارد.
تایپ رویداد uttapen.meta
تایپ ChatCompletionChunk فیلدهای cost_toman و balance_toman را ندارد. بهجای as any در همهجا، یک type guard کوچک بنویس تا هم خوانایی حفظ شود هم اگر روزی فیلدی عوض شد، کامپایلر خبر بدهد:
type UttapenMeta = { object: "uttapen.meta"; cost_toman: string; balance_toman: string; hold_toman: string };
function isMeta(chunk: unknown): chunk is UttapenMeta {
return typeof chunk === "object" && chunk !== null && (chunk as { object?: string }).object === "uttapen.meta";
}
for await (const chunk of stream) {
if (isMeta(chunk)) { console.log(chunk.cost_toman); continue; }
process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}
اگر این رویداد را لازم نداری، هدر X-Uttapen-Include-Meta را نفرست؛ آن وقت استریم دقیقاً تایپ رسمی SDK است و هیچ cast لازم نیست. هزینه را بعداً از داشبورد یا GET /v1/uttapen/usage بخوان.
Responses API
client.responses.create فعلاً 404 برمیگرداند؛ chat.completions را استفاده کن (migration).