SiCore TokenWorks
LLM APIAPI GatewayAggregation

چگونه بدون تغییر کد، بین ارائه‌دهندگان LLM جابه‌جا شویم

SiCore TokenWorks Team·2026-09-03

بیشتر مردم وقتی پروژه‌ای را شروع می‌کنند، به قابلیت انتقال بین ارائه‌دهندگان فکر نمی‌کنند. آن‌ها از هر فروشنده‌ای که ثبت‌نام در آن آسان‌تر باشد یک کلید می‌گیرند، یکپارچه‌سازی را می‌نویسند و منتشر می‌کنند. چند ماه بعد، یک بنچمارک می‌خوانند، یا یک صورتحساب را باز می‌کنند، یا به محدودیت نرخ برخورد می‌کنند و می‌خواهند چیز دیگری را امتحان کنند. آن‌جاست که درد ظاهر می‌شود، چون آن‌ها فرضیات مربوط به ارائه‌دهنده اول را در لایه HTTP جای داده‌اند.

راه‌حل یک کتابخانه انتزاعی نیست. یک قالب انتقال داده است. API تکمیل گفتگوی OpenAI به‌آرامی به پروتکل پیش‌فرض برای گفتگو با LLMها تبدیل شده است، و وقتی بر اساس آن کد بنویسید، جابه‌جایی بین ارائه‌دهندگان عمدتاً به تغییر دو رشته تبدیل می‌شود.

API‌ای که همه کپی کردند

OpenAI یک قرارداد ساده تعریف کرد: شما JSON را به /v1/chat/completions ارسال می‌کنید و در پاسخ یک رشته choices[0].message.content دریافت می‌کنید. درخواست به این شکل است:

curl https://api.openai.com/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -d '{
    "model": "gpt-4o",
    "messages": [{"role": "user", "content": "Explain DNS in one paragraph"}]
  }'

تقریباً هر مدل مهمی اکنون این گویش را صحبت می‌کند. DeepSeek، Qwen، ERNIE، Doubao و بقیه همگی endpointی ارائه می‌دهند که همان بدنه را می‌پذیرد و همان شکل را برمی‌گرداند. این یعنی کد کلاینت شما اهمیت نمی‌دهد که طرف مقابل چه کسی است. می‌توانید رشته model و endpoint را عوض کنید و هیچ چیز دیگری تغییر نمی‌کند.

اینجا همان فراخوانی است که به یک aggregator اشاره می‌کند که چندین مورد از این مدل‌ها را زیر یک base URL واحد ارائه می‌دهد:

curl https://api.token8341.com/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKENWORKS_API_KEY" \
  -d '{
    "model": "deepseek-chat",
    "messages": [{"role": "user", "content": "Explain DNS in one paragraph"}]
  }'

یک کلاینت، چندین مدل

در Python معمولاً SDK OpenAI را یک بار نمونه‌سازی می‌کنید و برای هر درخواست یک نام مدل ارسال می‌کنید. اگر ارائه‌دهنده شما از API سازگار پشتیبانی کند، base_url را یک بار تنظیم می‌کنید و همه چیز دیگر یکسان می‌ماند:

from openai import OpenAI

client = OpenAI(
    base_url="https://api.token8341.com/v1",
    api_key="sk-your-tokenworks-key",
)

def ask(model: str, prompt: str) -> str:
    resp = client.chat.completions.create(
        model=model,
        messages=[{"role": "user", "content": prompt}],
    )
    return resp.choices[0].message.content

# Same function, three different model families.
print(ask("gpt-4o", "Summarize this log for me."))
print(ask("deepseek-chat", "Summarize this log for me."))
print(ask("qwen-max", "Summarize this log for me."))

تابع تغییر نمی‌کند. رشته مدل تغییر می‌کند. تمام ترفند همین است. استریمینگ هم به همین شکل کار می‌کند:

stream = client.chat.completions.create(
    model="deepseek-chat",
    messages=[{"role": "user", "content": "Write a haiku about databases"}],
    stream=True,
)
for chunk in stream:
    delta = chunk.choices[0].delta.content or ""
    print(delta, end="")

فراخوانی تابع، حالت JSON و embeddingها همگی بر همان سطح سازگار سوار می‌شوند، پس هنگام جابه‌جایی به گفتگوی ساده محدود نیستید.

جابه‌جایی چه چیزی به شما نمی‌دهد

می‌خواهم درباره بخشی که به‌صورت رایگان تغییر نمی‌کند شفاف باشم. انتقال داده قابل حمل است؛ مدل‌ها این‌طور نیستند.

مدل‌های مختلف به پرامپت‌ها متفاوت پاسخ می‌دهند. یک system prompt که برای GPT-4o تنظیم شده ممکن است روی یک مدل استدلالی که ساختار متفاوتی می‌خواهد عملکرد ضعیف‌تری داشته باشد. پنجره‌های context متفاوت‌اند، گاهی به مقدار زیادی: ممکن است یک مدل 128k توکن بپذیرد در حالی که دیگری 32k می‌پذیرد. توکن‌سازها هم متفاوت‌اند، پس یک سند یکسان روی هر مدل هزینه توکن متفاوتی دارد. حداکثر طول خروجی هم یک پارامتر per-model است، نه per-protocol.

پس «جابه‌جایی بدون تغییر کد» در واقع یعنی «جابه‌جایی بدون بازنویسی کلاینت HTTP شما.» شما هنوز به خودتان مدیون یک اجرای ارزیابی هستید پیش از اینکه ترافیک production را به یک مدل جدید اشاره دهید. خبر خوب این است که API سازگار اجرای آن ارزیابی را ارزان می‌کند، چون هارنس فقط یک حلقه روی نام مدل‌هاست.

یک الگوی عملی

کاری که من واقعاً در پروژه‌های جانبی انجام می‌دهم نگه‌داشتن یک جدول config کوچک است، نه تغییرات کد:

MODELS = {
    "fast": "deepseek-chat",
    "smart": "qwen-max",
    "frontier": "gpt-4o",
}

def run(task, prompt):
    return ask(MODELS[task], prompt)

می‌خواهید برای خط fast یک مدل ارزان‌تر امتحان کنید؟ یک خط را تغییر دهید. می‌خواهید وقتی ارائه‌دهنده اول به محدودیت نرخ خورد به ارائه‌دهنده دوم fallback کنید؟ استثنا را بگیرید و با رشته مدل بعدی دوباره تلاش کنید. هیچ‌کدام از این‌ها به ساخت درخواست دست نمی‌زند.

همین الگوی fallback جایی است که یک endpoint چندمدلی ارزش خود را ثابت می‌کند. با یک base URL و یک کلید می‌توانید routing، retry و A/B testing را در میان ارائه‌دهندگان انجام دهید، همه از طریق همان دو خط راه‌اندازی کلاینت که روز اول نوشتید.

نکات عجیب پاسخ که باید مراقبشان باشید

انتقال داده سازگار است، اما چند تفاوت در سطح پاسخ همچنان نشت می‌کند. مدل‌های استدلالی گاهی یک فیلد اضافی مثل reasoning_content در کنار پیام عادی برمی‌گردانند. کدی که choices[0].message.content را می‌خواند بدون توجه به این موضوع همچنان کار می‌کند، اما ممکن است بخواهید آن متن استدلال را در یک نمای debug نمایش دهید. پیام‌های خطا هم متفاوت‌اند: یک ارائه‌دهنده یک رشته مفید insufficient_quota برمی‌گرداند، دیگری یک 429 خالی بدون بدنه. اگر در حال ساخت retry هستید، هر 429 یا 5xx را به‌عنوان «کمی صبر کن و دوباره تلاش کن» در نظر بگیرید، نه به‌عنوان متنی که باید متن مخصوص فروشنده را parse کنید.

هیچ‌کدام از این‌ها دلیلی برای قفل ماندن روی یک ارائه‌دهنده نیست. دلیلی است برای اینکه مدیریت خطای خود را عمومی نگه دارید و انتخاب مدل را یک متغیر کنید.

این همان موضعی است که SiCore TokenWorks اتخاذ می‌کند: یک endpoint سازگار با OpenAI در https://api.token8341.com/v1 که رشته مدل تنها چیزی است که هنگام جابه‌جایی بین GPT-4o، Claude، Gemini، DeepSeek، Qwen، ERNIE، Doubao، Spark و Pangu تغییر می‌کند.