→ همهٔ نوشته‌ها

یک API سازگار با OpenAI برای جایگزینی مستقیم — مهاجرت در دو خط

20 اوت 2026 · 6 دقیقه مطالعه · api, migration

unbleep یک API سازگار با OpenAI است، که در عمل یعنی مهاجرت دو خط است و هیچ وابستگی تازه‌ای ندارد. SDK رسمی، منطق تلاش مجدد، حلقهٔ استریم، حساب‌وکتاب توکن‌ها و هندلرهای خطایتان را نگه می‌دارید. چیزی که عوض می‌شود این است که درخواست‌ها به کجا می‌روند و کدام کلید احرازشان می‌کند. این نوشته اول خود جابه‌جایی را پوشش می‌دهد و بعد چهار موردی را که آن‌قدر متفاوت‌اند که اگر از آن‌ها خبر نداشته باشید، چیزی را می‌شکنند.

مهاجرت به API سازگار با OpenAI در دو خط

python
import os

from openai import OpenAI

client = OpenAI(
    base_url="https://unbleep.ai/v1",
    api_key=os.environ["UNBLEEP_API_KEY"],
)

resp = client.chat.completions.create(
    model="unbleep",
    messages=[{"role": "user", "content": "Summarise this incident report."}],
)
print(resp.choices[0].message.content)

اگر ترجیح می‌دهید اصلاً به کد دست نزنید، SDK هر دو مقدار را از متغیرهای محیطی می‌خواند، پس یک تغییر پیکربندی کافی است:

bash
export OPENAI_BASE_URL="https://unbleep.ai/v1"
export OPENAI_API_KEY="ub_live_9f2c..."

همین دو مقدار بیشتر اکوسیستمی را که روی این SDK ساخته شده پوشش می‌دهد — LangChain، LlamaIndex، Instructor، Vercel AI SDK، و هر چیزی که تنظیمی برای base URL داشته باشد. کلیدها پیشوند دارند تا نشت‌شان برای ابزارهای اسکن secret آشکار باشد: ub_live_ و ub_test_ هر دو از همان اعتبار پیش‌پرداخت و با همان نرخ به‌ازای هر توکن کسر می‌کنند. کلید تست یک اعتبارنامهٔ جداگانه و قابل‌ابطال با سقف پایین‌تر است — 15 درخواست در دقیقه به‌جای 60 درخواستِ خود حساب — نه یک سطح رایگان. هر دو را سمت سرور نگه دارید.

GET /v1/models کار می‌کند، پس ابزارهایی که برای پر کردن یک منوی کشویی مدل‌ها را فهرست می‌کنند به حالت خاصی نیاز ندارند.

انتخاب مدل

سه سطح. شناسه‌های تاریخ‌دار — unbleep-250811، unbleep-high-250811، unbleep-mini-250811 — به‌عنوان نام مستعار پذیرفته می‌شوند، اما امروز به همان بیلدی اشاره می‌کنند که شناسهٔ ساده، و پاسخ هم شناسهٔ ساده را برمی‌گرداند. آن‌ها را یک املای سازگار با آینده بدانید، نه تضمین تکرارپذیری؛ اگر یک ارزیابی باید تکرارپذیر باشد، خروجی‌ها را ثبت کنید، نه شناسهٔ مدل را.

| مدل | کانتکست | قیمت ورودی / خروجی به‌ازای هر 1M | توضیح | | --- | --- | --- | --- | | unbleep | 256K | $3.00 / $3.00 | پیش‌فرض. سطح استدلالی. | | unbleep-high | 1M* | $5.00 / $5.00 | بزرگ‌ترین کارها. سطح استدلالی. | | unbleep-mini | 32K | $1.00 / $1.00 | ارزان و سریع. مستقیم پاسخ می‌دهد، بدون رد استدلال. |

*بدنهٔ درخواست به 2,000,000 بایت — تقریباً 500k توکن — محدود است، پس یک فراخوانی تنها عملاً نمی‌تواند پنجرهٔ 1M را پر کند؛ هر چیزی بزرگ‌تر از آن با 413 payload_too_large برمی‌گردد.

سقف 32K در unbleep-mini همان چیزی است که کسانی را که از یک مدل با کانتکست 128K مهاجرت می‌کنند غافلگیر می‌کند: پرامپتی که قبلاً جا می‌شد حالا رد می‌شود. اگر بر اساس هزینه مسیریابی می‌کنید، بر اساس طول هم مسیریابی کنید.

python
def pick_model(prompt_chars: int) -> str:
    """~4 chars/token is a deliberate under-estimate; leave room for the completion."""
    est_tokens = prompt_chars // 4
    if est_tokens < 24_000:
        return "unbleep-mini"
    return "unbleep" if est_tokens < 200_000 else "unbleep-high"

استریمینگ

stream=True را تنظیم کنید و Server-Sent Events استاندارد می‌گیرید: هر رویداد یک chat.completion.chunk است که یک delta حمل می‌کند، و استریم با یک data: [DONE] تحت‌اللفظی تمام می‌شود. حلقهٔ فعلی‌تان بدون تغییر کار می‌کند.

bash
curl -N https://unbleep.ai/v1/chat/completions \
  -H "Authorization: Bearer $UNBLEEP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "unbleep",
    "messages": [{"role": "user", "content": "Explain the residual stream."}],
    "stream": true,
    "stream_options": {"include_usage": true}
  }'

شمار توکن‌ها را چه بخواهید چه نخواهید می‌گیرید: unbleep همیشه usage را از بک‌اند درخواست می‌کند و آن را عبور می‌دهد، و فقط یک stream_options: {"include_usage": false} صریح آن را در مسیر خروج حذف می‌کند. در هر صورت استریم با یک چانک پایانی تمام می‌شود که آرایهٔ choices خالی دارد — و مگر اینکه انصراف داده باشید، شیء usage پرشده را حمل می‌کند — و به همین دلیل است که حلقهٔ پایین پیش از دست زدن به choices آن را بررسی می‌کند.

فیلد reasoning_content

این تنها افزودهٔ واقعی به اسکیما است. unbleep و unbleep-high پیش از پاسخ دادن فکر می‌کنند، و آن زنجیرهٔ استدلال در reasoning_content برمی‌گردد که کنار content روی message (غیراستریم) یا delta (استریم) قرار می‌گیرد. بک‌اندهای بالادستی سر اینکه اسمش reasoning باشد یا reasoning_content اختلاف دارند؛ API آن را به reasoning_content یکسان‌سازی می‌کند تا همیشه فقط با یک شکل سروکار داشته باشید.

چون بخشی از اسکیمای OpenAI نیست، در تعریف‌های نوع (type stubs) SDK هم نیست. مدل‌های پاسخ فیلدهای اضافی را می‌پذیرند، پس دسترسی به attribute در زمان اجرا کار می‌کند — اما آن را با getattr بخوانید تا پاسخ mini، که هیچ ردی از استدلال ندارد، خطا نیندازد:

python
import sys

stream = client.chat.completions.create(
    model="unbleep",
    messages=[{"role": "user", "content": "Why did this detection rule misfire?"}],
    stream=True,
    stream_options={"include_usage": True},
)

usage = None
for chunk in stream:
    if not chunk.choices:          # final usage-only chunk
        usage = chunk.usage
        continue
    delta = chunk.choices[0].delta
    thought = getattr(delta, "reasoning_content", None)
    if thought:                    # trace to stderr, answer to stdout
        sys.stderr.write(thought)
    if delta.content:
        sys.stdout.write(delta.content)

if usage:
    details = usage.completion_tokens_details
    print(f"\nreasoning tokens: {getattr(details, 'reasoning_tokens', 0)}")

سه پیامد عملی:

python
resp = client.chat.completions.create(
    model="unbleep",
    messages=[{"role": "user", "content": "phishing or benign?"}],
    max_tokens=4,
    extra_body={"thinking": False},   # spend the budget on the answer, not the trace
)

هرگز reasoning_content را به‌عنوان محتوای assistant به نوبت بعدی گفت‌وگو برنگردانید. این خروجی تشخیصی است، نه تاریخچهٔ مکالمه، و بازپخش آن پاسخ بعدی را خراب می‌کند.

خطاها و دو نکتهٔ واقعاً دردسرساز

خطاها از همان ساختار (envelope) OpenAI استفاده می‌کنند — {"error": {"type", "code", "message"}} — پس بلوک‌های except فعلی‌تان همچنان کار می‌کنند. کدهای وضعیت همان‌طور که انتظار دارید نگاشت می‌شوند: 401 کلید نامعتبر، 422 مسدودشده توسط policy: strict، 429 محدودیت نرخ، 5xx خطای بالادستی قابل تلاش مجدد.

وضعیتی که احتمالاً هیچ‌وقت هندل نکرده‌اید 402، اتمام اعتبار است. حساب‌ها پیش‌پرداخت‌اند، پس نه مصرف مازادی در کار است و نه صورت‌حسابی — درخواست‌ها به‌سادگی متوقف می‌شوند تا وقتی که حساب را شارژ کنید. OpenAI اتمام سهمیه را با 429 اعلام می‌کند، که یعنی در مسیر مهاجرت ساده‌لوحانه، منطق بک‌آف شما یک 402 را تا ابد تکرار می‌کند. آن را یک وضعیت پایانی بدانید و برایش هشدار بگذارید.

نکتهٔ دوم: system_fingerprint برگردانده نمی‌شود. این فیلد بک‌اند سرویس‌دهنده را مشخص می‌کند، پس همراه با بقیهٔ فیلدهای اختصاصی فروشنده حذف می‌شود. اگر کش یا بررسی تکرارپذیری‌تان را روی آن کلید زده‌اید، به نشانگر نسخهٔ خودتان نیاز خواهید داشت: شناسه‌های تاریخ‌دار مدل نام مستعار بیلد فعلی‌اند، نه اسنپ‌شات‌های منجمد، پس به شما نمی‌گویند بک‌اند کی عوض شده است.

محدودیت‌های نرخ به‌صورت هدر روی هر پاسخ برمی‌گردند — x-ratelimit-limit-requests، x-ratelimit-remaining-requests، x-ratelimit-reset-requests — تا یک کار دسته‌ای بتواند نرخ درخواستش را تنظیم کند، به‌جای اینکه آن سقف را با برخورد به آن کشف کند. سقف دومی هم هست که هدرها توصیفش نمی‌کنند: حداکثر 8 درخواست همزمانِ در حال پردازش به‌ازای هر حساب، و نهمی با 429 و کد too_many_concurrent_requests برمی‌گردد. یک فراخوانی استریم جایگاهش را تا پایان استریم نگه می‌دارد، پس تعداد ورکرهای خودتان را روی 8 محدود کنید.

به چه چیزی وصل می‌شوید

بد نیست صریح بگوییم: مدل‌های پشت این اندپوینت abliterated هستند، یعنی رفتار امتناع‌شان در سطح وزن‌ها حذف شده است. اصلاً نکته همین است — این یک API برای توسعه‌دهندگان است که هدفش پژوهش امنیتی، ردتیمینگ و ارزیابی است، جایی که امتناع یک خطای اندازه‌گیری است. همچنین یعنی گاردریل‌های معمول آنجا نیستند که جلوی یک پرامپت بد را بگیرند، پس یک انسان را پاسخگوی خروجی‌ها نگه دارید و سیاست استفادهٔ مجاز را بخوانید. مسئولیت استفادهٔ قانونی با شماست.

یک کلید API بگیرید — مهاجرت واقعاً دو خط است.