uttapen

چطور کد OpenAI‌ات را با تغییر یک خط به یوتاپن وصل کنی (Python، Node، PHP)

راهنمای عملی تغییر base_url در SDK رسمی OpenAI برای Python، Node.js و PHP، با کد تست‌شده، خواندن هزینهٔ تومانی از هدر پاسخ و نکات مهاجرت بدون شکستن کد.

۱۶ شهریور ۱۴۰۵ · ۱۰ دقیقه مطالعه · تیم یوتاپن

قول «فقط یک خط عوض کن» را زیاد شنیده‌ای و معمولاً بعدش سه ساعت دیباگ می‌کنی. این مقاله قرار است همان یک خط را برای Python، Node.js و PHP دقیقاً نشان بدهد، و مهم‌تر از آن، جاهایی را که ممکن است بعد از آن یک خط گیر کنی از قبل بگوید. همهٔ قطعه‌کدها با نسخه‌های فعلی SDKها (openai پایتون ۳٫x، openai نود ۷٫x، openai-php/client ۰٫۲۰) علیه دروازهٔ ما اجرا شده‌اند و خروجی‌شان همین‌جا آمده است.

چرا اصلاً یک خط کافی است

دروازهٔ uttapen قرارداد HTTP OpenAI را پیاده کرده: همان مسیرها (/v1/chat/completions، /v1/embeddings، /v1/models)، همان شکل بدنهٔ درخواست، همان ساختار پاسخ و همان قالب خطاها. SDK رسمی OpenAI از روز اول یک پارامتر base_url داشته، چون خود OpenAI هم برای محیط Azure و پروکسی‌های سازمانی به آن نیاز داشت. ما فقط از همان در ورودی استفاده می‌کنیم. بنابراین کتابخانه‌هایی مثل LangChain، LlamaIndex، Vercel AI SDK و ابزارهایی مثل Cursor و Continue که روی SDK یا قرارداد OpenAI سوارند، بدون تغییر کد داخلی‌شان با ما کار می‌کنند.

دو تفاوتی که باید بدانی: نام مدل‌ها با پیشوند ارائه‌دهنده است (openai/gpt-5-mini به‌جای gpt-5-mini)، و کلید با sk-up- شروع می‌شود. پیشوند sk- عمداً حفظ شده تا ابزارهایی که شکل کلید را اعتبارسنجی می‌کنند، خطا ندهند.

راه صفرخطی: متغیر محیطی

قبل از دست زدن به کد، این را امتحان کن. SDK پایتون و نود هر دو علاوه بر OPENAI_API_KEY، متغیر OPENAI_BASE_URL را هم می‌خوانند. یعنی اگر کدت OpenAI() را بدون آرگومان می‌سازد، اصلاً لازم نیست چیزی را عوض کنی:

export OPENAI_API_KEY="sk-up-…"
export OPENAI_BASE_URL="https://api.uttapen.ir/v1"

این را با هر دو SDK تست کردیم؛ OpenAI() خالی در پایتون و new OpenAI() خالی در نود، هر دو به دروازهٔ ما وصل شدند و مدل qwen/qwen3-coder جواب داد. برای سرورهایی که چند سرویس مختلف روی یک ماشین دارند، بهتر است به‌جای متغیر سراسری، مقدار را در فایل .env همان پروژه بگذاری و صریحاً به سازنده پاس بدهی؛ روش صریح در ادامه است.

Python

نصب یا به‌روزرسانی: pip install -U openai. سپس:

import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.uttapen.ir/v1",      # ← همان یک خط
    api_key=os.environ["UTTAPEN_API_KEY"],
)

resp = client.chat.completions.create(
    model="openai/gpt-5-mini",
    messages=[
        {"role": "system", "content": "فقط فارسی جواب بده."},
        {"role": "user", "content": "سه مزیت کش کردن پاسخ API را نام ببر."},
    ],
    max_tokens=300,
)
print(resp.choices[0].message.content)
print("tokens:", resp.usage.prompt_tokens, "+", resp.usage.completion_tokens)

اگر می‌خواهی بدانی این درخواست دقیقاً چند تومان شد، لازم نیست حساب کنی؛ دروازه در هدر پاسخ می‌گوید. SDK پایتون برای دسترسی به هدرها with_raw_response دارد:

raw = client.chat.completions.with_raw_response.create(
    model="deepseek/deepseek-chat",
    messages=[{"role": "user", "content": "یک جمله دربارهٔ Postgres بگو."}],
)
resp = raw.parse()
print(resp.choices[0].message.content)
print("هزینهٔ این درخواست:", raw.headers["x-uttapen-cost-toman"], "تومان")
print("موجودی بعد از آن:", raw.headers["x-uttapen-balance-toman"], "تومان")
print("request id:", raw.headers["x-uttapen-request-id"])

خروجی واقعی اجرای ما:

هزینهٔ این درخواست: 3.618650 تومان
موجودی بعد از آن: 97457.693961 تومان
request id: 01a078e0-2647-773f-8a8c-35c90fb850f2

X-Uttapen-Request-Id را در لاگ خودت نگه دار؛ اگر روزی دربارهٔ یک درخواست خاص سؤالی داشتی، با همین شناسه پیگیری می‌شود. جزئیات SDK پایتون، از جمله نسخهٔ async، در مستندات پایتون است.

Node.js

نصب: npm install openai. کد با ESM:

import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api.uttapen.ir/v1",        // ← همان یک خط (دقت کن: baseURL نه base_url)
  apiKey: process.env.UTTAPEN_API_KEY,
});

const resp = await client.chat.completions.create({
  model: "anthropic/claude-sonnet-4.5",
  messages: [{ role: "user", content: "تفاوت let و const را در یک جمله بگو." }],
  max_tokens: 200,
});
console.log(resp.choices[0].message.content);
console.log("usage:", resp.usage);

شیء usage که برمی‌گردد تعداد توکن‌های ورودی و خروجی را می‌دهد؛ رقم تومانی درخواست در هدر پاسخ است. برای خواندن هدرها در نود از withResponse() استفاده کن:

const { data, response } = await client.chat.completions
  .create({ model: "google/gemini-2.5-flash", messages: [{ role: "user", content: "سلام" }] })
  .withResponse();

console.log(data.choices[0].message.content);
console.log("cost (toman):", response.headers.get("x-uttapen-cost-toman"));
console.log("balance (toman):", response.headers.get("x-uttapen-balance-toman"));

یک اشتباه رایج در نود: نوشتن base_url با زیرخط، که SDK نود بی‌صدا نادیده می‌گیرد و درخواستت به api.openai.com می‌رود و 401 می‌گیری. نام درست baseURL است. راهنمای کامل در مستندات نود.

PHP و Laravel

در PHP دو راه داری: کلاینت رسمی جامعه (openai-php/client) یا Guzzle خام. برای Laravel معمولاً پکیج openai-php/laravel نصب می‌شود که همان کلاینت را با یک Facade می‌دهد.

composer require openai-php/client guzzlehttp/guzzle
<?php
require __DIR__ . '/vendor/autoload.php';

$client = OpenAI::factory()
    ->withBaseUri('https://api.uttapen.ir/v1')     // ← همان یک خط
    ->withApiKey(getenv('UTTAPEN_API_KEY'))
    ->make();

$result = $client->chat()->create([
    'model' => 'deepseek/deepseek-chat-v3.1',
    'messages' => [
        ['role' => 'user', 'content' => 'یک توضیح یک‌خطی برای middleware در لاراول بنویس.'],
    ],
]);

echo $result->choices[0]->message->content, PHP_EOL;
echo 'tokens: ', $result->usage->promptTokens, ' + ', $result->usage->completionTokens, PHP_EOL;

اگر از openai-php/laravel استفاده می‌کنی، فقط در config/openai.php مقدار base_uri را به https://api.uttapen.ir/v1 بگذار و api_key را از .env بخوان؛ بقیهٔ کد OpenAI::chat()->create([...]) بدون تغییر می‌ماند.

اگر وابستگی اضافه نمی‌خواهی، Guzzle به‌تنهایی کافی است و دسترسی به هدرها هم مستقیم‌تر است:

<?php
use GuzzleHttp\Client;

$http = new Client(['base_uri' => 'https://api.uttapen.ir/v1/']);   // اسلش انتها مهم است

$res = $http->post('chat/completions', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('UTTAPEN_API_KEY')],
    'json' => [
        'model' => 'openai/gpt-4o-mini',
        'messages' => [['role' => 'user', 'content' => 'سلام']],
    ],
]);

$body = json_decode((string) $res->getBody(), true);
echo $body['choices'][0]['message']['content'], PHP_EOL;
echo 'cost: ', $res->getHeaderLine('X-Uttapen-Cost-Toman'), ' toman', PHP_EOL;

خروجی اجرای ما: cost: 2.175938 toman. نکتهٔ Guzzle: اگر base_uri بدون اسلش پایانی باشد، مسیر نسبی chat/completions بخش v1 را حذف می‌کند و 404 می‌گیری. این یکی از همان گیرهای سه‌ساعته است.

Go و بقیه

SDK رسمی Go (github.com/openai/openai-go) با option.WithBaseURL("https://api.uttapen.ir/v1") وصل می‌شود. برای curl فقط آدرس و هدر Authorization عوض می‌شود. نمونهٔ آمادهٔ همهٔ زبان‌ها با مدل دلخواه:

curl https://api.uttapen.ir/v1/chat/completions \
  -H "Authorization: Bearer sk-up-…" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "google/gemini-2.5-flash",
    "messages": [{"role": "user", "content": "سلام! خودت را معرفی کن."}],
    "stream": true
  }'

کتابخانه‌هایی که روی OpenAI سوارند

بیشتر اکوسیستم هوش مصنوعی امروز به‌جای اینکه با هر ارائه‌دهنده جداگانه حرف بزند، قرارداد OpenAI را به‌عنوان زبان مشترک پذیرفته. این یعنی همان تغییر یک خطی در آن‌ها هم جواب می‌دهد، فقط باید بدانی آن خط کجاست.

در LangChain کلاس ChatOpenAI پارامتر base_url می‌گیرد و model را هم با پیشوند ارائه‌دهنده می‌فرستی؛ زنجیره‌ها، ابزارها و agentها بدون تغییر کار می‌کنند، چون همه از همین کلاینت عبور می‌کنند. در LlamaIndex معادلش OpenAI(api_base=...) است. در Vercel AI SDK به‌جای openai() پیش‌فرض، از createOpenAI({ baseURL, apiKey }) استفاده می‌کنی و همان streamText و generateText را صدا می‌زنی؛ بخش استریم را در مقالهٔ SSE با کد کامل دیده‌ای. در Cursor و Continue در تنظیمات مدل، گزینهٔ «OpenAI-compatible» را انتخاب می‌کنی و آدرس و کلید را می‌دهی؛ برای Cursor باید نام مدل را دستی اضافه کنی چون فهرست پیش‌فرضش نام‌های بدون پیشوند دارد. Open WebUI هم در بخش Connections یک اتصال OpenAI جدید می‌گیرد و بعد از ذخیره، مدل‌ها را خودش از /v1/models می‌خواند.

یک نکته دربارهٔ همهٔ این ابزارها: بعضی‌شان قبل از اولین درخواست، GET /v1/models را صدا می‌زنند تا فهرست مدل‌ها را بسازند. این مسیر در دروازهٔ ما بدون احراز هویت هم جواب می‌دهد و پنج دقیقه کش می‌شود، پس اگر مدلی تازه اضافه شده و در فهرست ابزارت نیست، چند دقیقه صبر کن یا نامش را دستی وارد کن.

چک‌لیست مهاجرت

بعد از تغییر آن یک خط، این‌ها را چک کن تا در production غافلگیر نشوی:

  1. نام مدل‌ها. هر جای کد که "gpt-4o" یا "gpt-4o-mini" هاردکد شده، باید openai/ جلویش بیاید. راه تمیز: یک ثابت MODEL در تنظیمات، نه رشتهٔ پراکنده. فهرست کامل نام‌ها با GET /v1/models یا در صفحهٔ مدل‌ها.
  2. مدل‌هایی که وجود ندارند. اگر کدت به gpt-3.5-turbo وابسته است، معادل ارزان امروزی GPT-5 nano یا GPT-4o mini است. مدل ناموجود 404 model_not_found می‌دهد، نه خطای مبهم.
  3. Responses API. اگر از client.responses.create استفاده می‌کنی، فعلاً باید به chat.completions برگردی؛ مسیر /v1/responses هنوز فعال نیست و با پیام راهنما 404 برمی‌گرداند.
  4. خطای ۴۰۲. این کد در OpenAI وجود ندارد. جایی که RateLimitError را می‌گیری، یک شاخه برای APIStatusError با status_code == 402 اضافه کن تا کاربر نهایی پیام «موجودی کافی نیست» ببیند، نه «خطای ناشناخته». لیست خطاها در صفحهٔ کدهای خطا.
  5. max_tokens بزرگ. برآورد هزینه قبل از ارسال بر اساس max_tokens است. اگر عادت داری max_tokens=16000 بگذاری «که کم نیاید»، روی مدل‌های گران ممکن است با موجودی کافی هم 402 بگیری. عدد واقع‌بینانه بگذار؛ هزینهٔ نهایی همیشه بر اساس مصرف واقعی است.
  6. Timeout. مسیر ایران به ارائه‌دهنده از relay خارج از کشور می‌گذرد و حدود ۱۰۰ میلی‌ثانیه به زمان تا اولین توکن اضافه می‌کند. اگر timeout کلاینتت ۲ ثانیه است، برای مدل‌های استدلالی کم است؛ برای درخواست‌های غیراستریم ۶۰ ثانیه و برای استریم بدون timeout کلی (فقط timeout اتصال) توصیه می‌شود.
  7. سقف نرخ. پیش‌فرض هر کلید ۶۰ درخواست در دقیقه است و در هدرهای X-RateLimit-* گزارش می‌شود. SDK پایتون و نود 429 را خودشان با backoff تکرار می‌کنند؛ در PHP خودت باید Retry-After را بخوانی. محدودیت نرخ را ببین.
  8. Embeddings. مسیر /v1/embeddings کار می‌کند ولی مدل‌های embedding در کاتالوگ فعلی OpenRouter محدودند؛ قبل از مهاجرت پایپ‌لاین RAG، فهرست را چک کن.

مهاجرت تدریجی، نه یک‌شبه

اگر محصولت زنده است و کاربر دارد، همه‌چیز را یک‌جا عوض نکن. الگویی که خودمان توصیه می‌کنیم سه مرحله دارد.

مرحلهٔ اول، دو کلاینت کنار هم: یک کلاینت با تنظیمات قبلی و یکی با تنظیمات یوتاپن، و یک متغیر محیطی مثل LLM_UPSTREAM که تصمیم می‌گیرد کدام استفاده شود. تغییر و بازگشت با یک deploy کوچک انجام می‌شود، بدون دست زدن به منطق کسب‌وکار.

مرحلهٔ دوم، درصدی از ترافیک: مثلاً ده درصد درخواست‌ها را از مسیر جدید بفرست و سه چیز را با هم مقایسه کن: زمان تا اولین توکن، نرخ خطا، و کیفیت پاسخ برای چند پرامپت مرجع که خودت داری. چون مدل پشت هر دو مسیر یکی است، تفاوت کیفیت نباید ببینی؛ اگر دیدی، احتمالاً پارامتری مثل temperature یا پیام system در یکی از مسیرها فرق دارد.

مرحلهٔ سوم، حذف مسیر قدیمی: وقتی یک هفته بدون تفاوت معنادار گذشت. کلید قبلی را لغو کن تا اگر جایی از کد هنوز به آن اشاره دارد، همان لحظه در staging خطا بدهد نه ماه بعد در production.

هزینه را در لاگ خودت داشته باش

چون هر پاسخ هدر X-Uttapen-Cost-Toman دارد، می‌توانی بدون هیچ فراخوانی اضافه، هزینهٔ هر درخواست را کنار شناسهٔ کاربر خودت لاگ کنی. در پایتون ساده‌ترین راه، یک تابع کوچک دور with_raw_response است که هزینه را به لاگر ساخت‌یافته می‌دهد؛ در نود یک تابع دور withResponse()؛ در لاراول یک Middleware یا یک Listener روی رویداد درخواست. بعد از یک هفته، جمع همین اعداد به تفکیک کاربر یا قابلیت، دقیق‌ترین گزارشی است که می‌توانی داشته باشی و برای تصمیم‌هایی مثل «این قابلیت را با مدل ارزان‌تر بدهیم» بی‌اندازه مفید است. داشبورد ما همین را به تفکیک کلید و مدل و روز نشان می‌دهد، ولی به تفکیک کاربرِ محصول تو فقط از لاگ خودت درمی‌آید.

چیزهایی که عوض نمی‌شود

استریم، tools، response_format با JSON schema، تصویر در messages، پارامترهای temperature و seed، همه همان‌طور که در OpenAI می‌فرستی عبور می‌کنند؛ دروازه بدنهٔ درخواست را دست نمی‌زند و فقط هدر Authorization را با اعتبار خودش جایگزین می‌کند. اگر پارامتری را مدلی پشتیبانی نکند، همان خطای 400 ارائه‌دهنده به تو می‌رسد؛ فیلد supported_parameters در پاسخ /v1/models از قبل می‌گوید هر مدل چه چیزهایی را قبول می‌کند.

یک تست کوچک قبل از اینکه بروی

بعد از تغییر، این سه خط را در محیط staging اجرا کن: یک درخواست غیراستریم و چاپ هدر هزینه، یک درخواست استریم که تا [DONE] برسد، و یک درخواست عمدی با کلید غلط که 401 بگیری. اگر هر سه همان‌طور که انتظار داری رفتار کردند، مهاجرت تمام است. برای اینکه کلید تست داشته باشی، وارد داشبورد شو و یک کلید جدا با سقف ماهانهٔ کوچک برای staging بساز؛ کلید production را هرگز در محیط تست استفاده نکن.

مقاله‌های مرتبط

می‌خواهی همین کد را اجرا کنی؟

با شماره موبایل ثبت‌نام کن، کیف پول را شارژ کن و کلید بگیر. ساخت کلید API ←