پرش به محتوا
هوشی

API یکپارچه

Chat Completions: یک API برای همهٔ مدل‌ها

endpoint /v1/chat/completions هوشی همان قرارداد OpenAI را دارد، اما با تغییر فیلد model می‌توانی بین GPT، Claude، Gemini، Grok، DeepSeek، Kimi، Qwen و ده‌ها مدل دیگر جابه‌جا شوی؛ هوشی پیام‌ها، ابزارها و تصاویر را به فرمت هر پروایدر ترجمه می‌کند.

POSThttps://api.hooshi.ai/v1/chat/completions

درخواست پایه

# pip install openai
from openai import OpenAI

client = OpenAI(
    base_url="https://api.hooshi.ai/v1",
    api_key="hk-...",  # کلید هوشی از /console
)

resp = client.chat.completions.create(
    model="anthropic/claude-sonnet-5-5",  # یا openai/gpt-5.5 ، xai/grok-4.7 ، ...
    messages=[
        {"role": "system", "content": "تو یک دستیار برنامه‌نویسی دقیق هستی."},
        {"role": "user", "content": "یک تابع پایتون برای تبدیل اعداد به حروف فارسی بنویس."},
    ],
)

print(resp.choices[0].message.content)
print("هزینه:", resp.model_extra["hooshi"]["cost"], "تومان")

انتخاب مدل

فیلد model را می‌توانی به دو شکل بفرستی. شناسهٔ کامل provider/model همیشه دقیق‌ترین انتخاب است:

شکلمثالتوضیح
شناسهٔ هوشیanthropic/claude-opus-5-5پیشنهادی؛ یکتا و بدون ابهام.
شناسهٔ بالادستی (bare)gpt-5.5شناسهٔ خود پروایدر؛ اگر چند پروایدر یک مدل را داشته باشند، اولین مورد در ترتیب کاتالوگ انتخاب می‌شود.
نام مستعار (alias)—اگر برای مدلی نام مستعار تعریف شده باشد، آن هم پذیرفته می‌شود (بعد از دو حالت بالا بررسی می‌شود).

فیلد model در پاسخ همان مقداری است که فرستاده‌ای و شناسهٔ دقیق مدل اجراشده در hooshi.model برمی‌گردد. فهرست کامل در مدل‌ها و شناسه‌ها.

پارامترهای درخواست

پارامترنوعتوضیح
modelالزامیstringشناسهٔ مدل، مثلاً openai/gpt-5.5. نوع مدل باید chat باشد.
messagesالزامیarrayآرایهٔ پیام‌ها با نقش‌های system، developer، user، assistant و tool. محتوا می‌تواند رشته یا آرایه‌ای از partها (text، image_url، file) باشد.
streamاختیاریbooleanاگر true باشد پاسخ به‌صورت Server-Sent Events استریم می‌شود. پیش‌فرض false.
stream_options.include_usageاختیاریbooleanدر حالت استریم، یک chunk پایانی با usage (و choices خالی) قبل از [DONE] ارسال می‌شود.
max_tokens / max_completion_tokensاختیاریintegerسقف توکن خروجی. هر دو نام پذیرفته می‌شوند و برای هر پروایدر به نام درست تبدیل می‌شوند. برای Claude اگر نفرستی، پیش‌فرض متناسب با مدل گذاشته می‌شود.
temperatureاختیاریnumber۰ تا ۲ (برای Claude حداکثر ۱). مدل‌های استدلالی OpenAI و Claudeهای دارای تفکر تطبیقی این پارامتر را نادیده می‌گیرند.
top_pاختیاریnumberنمونه‌برداری هسته‌ای. مانند temperature برای مدل‌های استدلالی OpenAI حذف می‌شود.
frequency_penalty / presence_penaltyاختیاریnumberجریمهٔ تکرار؛ برای پروایدرهای سازگار با OpenAI ارسال می‌شود.
stopاختیاریstring | string[]توالی‌های توقف. برای Claude به stop_sequences تبدیل می‌شود.
seedاختیاریintegerبرای خروجی قابل‌تکرار (در پروایدرهایی که پشتیبانی می‌کنند).
toolsاختیاریarrayتعریف توابع با فرمت OpenAI (type: "function"). برای Claude به‌طور خودکار به input_schema تبدیل می‌شود.
tool_choiceاختیاریstring | objectauto، none، required یا یک تابع مشخص. برای Claude فقط none اعمال می‌شود و بقیه به auto برمی‌گردد.
parallel_tool_callsاختیاریbooleanfalse یعنی حداکثر یک فراخوانی ابزار در هر نوبت (برای Claude هم پشتیبانی می‌شود).
response_formatاختیاریobject{"type": "json_schema", ...} یا json_object. برای Claude فقط json_schema پشتیبانی می‌شود.
reasoning_effortاختیاریstringnone، minimal، low، medium، high، xhigh، max؛ فقط روی مدل‌های استدلالی اثر دارد. جدول نگاشت.
web_searchاختیاریbooleanپارامتر اختصاصی هوشی: برای Claudeهای دارای قابلیت جستجوی وب، ابزار web_search را فعال می‌کند. منابع در فیلد citations برمی‌گردند.
web_search_optionsاختیاریobjectجستجوی وب برای مدل‌های search در OpenAI (فرمت OpenAI).
search_domain_filter / search_recency_filter / return_images / return_related_questionsاختیاریmixedپارامترهای اختصاصی Perplexity (Sonar) که مستقیم عبور داده می‌شوند.
n, logprobs, top_logprobs, logit_bias, prediction, modalities, audioاختیاریmixedبرای پروایدرهای سازگار با OpenAI بدون تغییر ارسال می‌شوند.
userاختیاریstringشناسهٔ کاربر نهایی تو برای ردیابی سوءاستفاده؛ اگر نفرستی، شناسهٔ داخلی کلید ارسال می‌شود.

ساختار پاسخ

JSON
{
  "id": "chatcmpl-r1Vq9Lk2P0aZ...",
  "object": "chat.completion",
  "created": 1791590400,
  "model": "openai/gpt-5.5",
  "choices": [{
    "index": 0,
    "message": {
      "role": "assistant",
      "content": "...",
      "tool_calls": [ ... ],          // فقط وقتی مدل ابزار صدا بزند
      "reasoning_content": "..."      // فقط وقتی پروایدر خلاصهٔ استدلال برگرداند
    },
    "finish_reason": "stop",           // stop | length | tool_calls | content_filter
    "logprobs": null
  }],
  "usage": {
    "prompt_tokens": 1520,
    "completion_tokens": 410,
    "total_tokens": 1930,
    "prompt_tokens_details": { "cached_tokens": 1024 }
  },
  "citations": [{ "title": "...", "url": "https://..." }],   // فقط برای مدل‌های جستجو
  "hooshi": { "model": "openai/gpt-5.5", "cost": 2374.083, "currency": "IRT" }
}

هدر X-Request-Id هم در هر پاسخ برمی‌گردد (همان مقدار id)؛ در تماس با پشتیبانی آن را بفرست.

شیء hooshi (هزینهٔ تومانی)

فیلدتوضیح
hooshi.modelشناسهٔ دقیق مدلی که اجرا شد (حتی اگر شناسهٔ bare یا alias فرستاده باشی).
hooshi.costمبلغی که برای همین درخواست از کیف پول API کسر شد، به تومان با ۴ رقم اعشار.
hooshi.currencyهمیشه IRT (تومان).

SDKهای رسمی فیلدهای اضافه را حذف نمی‌کنند: در پایتون از resp.model_extra["hooshi"] و در Node.js از (resp as any).hooshi بخوان. در حالت استریم این شیء ارسال نمی‌شود؛ هزینه را از گزارش مصرف یا محاسبه با usage بگیر.

استریم (SSE)

با "stream": true پاسخ با Content-Type: text/event-stream و دقیقاً همان قالب chunkهای OpenAI می‌آید؛ پس هر کتابخانه‌ای که استریم OpenAI را می‌فهمد، با همهٔ مدل‌های هوشی هم کار می‌کند.

stream = client.chat.completions.create(
    model="xai/grok-4.7",
    messages=[{"role": "user", "content": "یک داستان خیلی کوتاه بنویس."}],
    stream=True,
    stream_options={"include_usage": True},
)

for chunk in stream:
    if chunk.choices and chunk.choices[0].delta.content:
        print(chunk.choices[0].delta.content, end="", flush=True)
    if chunk.usage:  # آخرین chunk: choices خالی + usage
        print("\n", chunk.usage.total_tokens, "tokens")

قالب رویدادها

text/event-stream
data: {"id":"chatcmpl-a1","object":"chat.completion.chunk","created":1791590400,"model":"xai/grok-4.7","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"logprobs":null}]}

data: {"id":"chatcmpl-a1","object":"chat.completion.chunk","created":1791590400,"model":"xai/grok-4.7","choices":[{"index":0,"delta":{"content":"یکی بود"},"finish_reason":null,"logprobs":null}]}

data: {"id":"chatcmpl-a1","object":"chat.completion.chunk","created":1791590400,"model":"xai/grok-4.7","choices":[{"index":0,"delta":{"content":"، یکی نبود…"},"finish_reason":null,"logprobs":null}]}

data: {"id":"chatcmpl-a1","object":"chat.completion.chunk","created":1791590400,"model":"xai/grok-4.7","choices":[{"index":0,"delta":{},"finish_reason":"stop","logprobs":null}]}

data: {"id":"chatcmpl-a1","object":"chat.completion.chunk","created":1791590400,"model":"xai/grok-4.7","choices":[],"usage":{"prompt_tokens":24,"completion_tokens":311,"total_tokens":335,"prompt_tokens_details":{"cached_tokens":0}}}

data: [DONE]
  • اولین chunk همیشه delta.role = "assistant" دارد.
  • متن در delta.content، خلاصهٔ استدلال (در مدل‌هایی که می‌فرستند) در delta.reasoning_content و فراخوانی ابزار در delta.tool_calls می‌آید. اندیس tool_callها همیشه از ۰ شماره‌گذاری می‌شود.
  • chunk حاوی usage فقط وقتی stream_options.include_usage را true کرده باشی ارسال می‌شود.
  • اگر وسط استریم خطایی از پروایدر رخ دهد، یک رویداد {"error": {...}} و سپس [DONE] می‌آید (جزئیات).

Tool Calling (Function Calling)

ابزارها را با فرمت OpenAI تعریف کن؛ هوشی برای Claude آن‌ها را به tools با input_schema تبدیل می‌کند، tool_useها را به tool_calls برمی‌گرداند و پیام‌های role: tool را بهtool_result ترجمه می‌کند. یعنی یک کد واحد برای GPT، Claude، Gemini، Grok و DeepSeek.

import json
from openai import OpenAI

client = OpenAI(base_url="https://api.hooshi.ai/v1", api_key="hk-...")

tools = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "دمای فعلی یک شهر را برمی‌گرداند",
        "parameters": {
            "type": "object",
            "properties": {"city": {"type": "string", "description": "نام شهر، مثلاً Tehran"}},
            "required": ["city"],
        },
    },
}]

messages = [{"role": "user", "content": "هوای تهران و شیراز الان چطوره؟"}]
resp = client.chat.completions.create(model="anthropic/claude-sonnet-5-5", messages=messages, tools=tools)
msg = resp.choices[0].message

if msg.tool_calls:
    messages.append(msg)  # پیام دستیار همراه با tool_calls
    for call in msg.tool_calls:
        args = json.loads(call.function.arguments)
        result = {"city": args["city"], "temp_c": 24}  # ← اینجا تابع واقعی خودت را صدا بزن
        messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})
    final = client.chat.completions.create(model="anthropic/claude-sonnet-5-5", messages=messages, tools=tools)
    print(final.choices[0].message.content)

پاسخ وقتی مدل ابزار صدا می‌زند:

JSON
{
  "choices": [{
    "index": 0,
    "message": {
      "role": "assistant",
      "content": "",
      "tool_calls": [{
        "id": "toolu_01A9q...",
        "type": "function",
        "function": { "name": "get_weather", "arguments": "{\"city\":\"Tehran\"}" }
      }]
    },
    "finish_reason": "tool_calls"
  }]
}

تفاوت‌های Claude

مدل‌های فعلی Claude استفادهٔ اجباری از ابزار را نمی‌پذیرند؛ بنابراین tool_choice: "required" یا انتخاب یک تابع مشخص نادیده گرفته می‌شود و مدل خودش تصمیم می‌گیرد. tool_choice: "none" و parallel_tool_calls: false پشتیبانی می‌شوند.

Vision: ارسال تصویر

برای مدل‌هایی که قابلیت vision دارند (در فهرست مدل‌ها مشخص است)، تصویر را با part از نوع image_url بفرست؛ هم URL عمومی و هم data URL (base64) پذیرفته می‌شود. برای مدلی که تصویر را پشتیبانی نمی‌کند، تصویر با یک متن جایگزین حذف می‌شود.

import base64

img = base64.b64encode(open("receipt.jpg", "rb").read()).decode()

resp = client.chat.completions.create(
    model="gemini/gemini-3.1-pro-preview",
    messages=[{
        "role": "user",
        "content": [
            {"type": "text", "text": "مبلغ کل و تاریخ این رسید را استخراج کن."},
            {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{img}"}},
        ],
    }],
)
print(resp.choices[0].message.content)

فایل PDF هم با part از نوع file (file.file_data به‌صورت data:application/pdf;base64,…) برای OpenAI و Claude پشتیبانی می‌شود.

خروجی ساختاریافته (JSON Schema)

با response_format از نوع json_schema مدل مجبور می‌شود خروجی‌ای مطابق schema تو تولید کند. این قابلیت روی OpenAI، Claude (از طریق output_config.format) و بیشتر مدل‌های سازگار کار می‌کند.{"type": "json_object"} هم برای پروایدرهای سازگار با OpenAI ارسال می‌شود.

resp = client.chat.completions.create(
    model="anthropic/claude-sonnet-5-5",
    messages=[{"role": "user", "content": "از این متن نام، شهر و سن را دربیار: «سارا ۲۸ ساله از اصفهان است.»"}],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "person",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "city": {"type": "string"},
                    "age": {"type": "integer"},
                },
                "required": ["name", "city", "age"],
                "additionalProperties": False,
            },
        },
    },
)
import json
print(json.loads(resp.choices[0].message.content))  # {'name': 'سارا', 'city': 'اصفهان', 'age': 28}

reasoning_effort (میزان تفکر)

برای مدل‌های دارای قابلیت reasoning می‌توانی عمق تفکر را تنظیم کنی. مقادیر یکسان را بفرست؛ هوشی آن‌ها را به معادل هر پروایدر تبدیل می‌کند. روی مدل‌های غیراستدلالی این پارامتر نادیده گرفته می‌شود. توکن‌های تفکر جزو توکن خروجی محاسبه می‌شوند.

مقدار ارسالیOpenAIAnthropic (Claude)سایر پروایدرها
none / minimalminimallowlow
lowlowlowlow
mediummediummediummedium
highhighhighhigh
xhighxhighxhighhigh
maxxhighmaxhigh
Python
resp = client.chat.completions.create(
    model="openai/gpt-6.1-sol",
    reasoning_effort="high",
    messages=[{"role": "user", "content": "این الگوریتم را از نظر پیچیدگی زمانی تحلیل کن: ..."}],
)
print(resp.choices[0].message.content)
print(getattr(resp.choices[0].message, "reasoning_content", None))  # خلاصهٔ تفکر، اگر موجود باشد

Claudeهای استدلالی با «تفکر تطبیقی» اجرا می‌شوند و خلاصهٔ تفکرشان در reasoning_content برمی‌گردد. در این حالت temperature اعمال نمی‌شود.

تزریق هویت مدل و غیرفعال‌کردن آن

بسیاری از مدل‌ها وقتی از آن‌ها پرسیده شود «تو کدام مدلی؟» جواب نادرست می‌دهند (مثلاً نسخهٔ قدیمی‌تر خودشان را نام می‌برند). برای اینکه کاربران تو همیشه پاسخ درست بگیرند، هوشی به‌طور پیش‌فرض یک سیستم‌پرامپت کوتاه به ابتدای گفتگو اضافه می‌کند که:

  • نام دقیق مدل و سازنده‌اش را مشخص می‌کند (مثلاً «تو Claude Opus 5.5 ساخت Anthropic هستی»)؛
  • تاریخ امروز را به میلادی و شمسی می‌گوید؛
  • از مدل می‌خواهد به زبان کاربر پاسخ دهد و فارسی را روان و با نیم‌فاصلهٔ درست بنویسد.

اگر خودت پیام system یا developer داری، متن هویت با جداکنندهٔ --- قبل از پیام تو در همان پیام ادغام می‌شود (برای سازگاری با پروایدرهایی که فقط یک system می‌پذیرند). این متن چند صد توکن ورودی اضافه می‌کند که در usage و هزینه دیده می‌شود.

غیرفعال‌کردن

دو راه داری:

  • برای یک درخواست: هدر X-Hooshi-Identity: off (مقادیر false و 0 هم پذیرفته می‌شوند).
  • برای یک کلید: گزینهٔ «تزریق هویت مدل» را در کنسول خاموش کن.
client = OpenAI(
    base_url="https://api.hooshi.ai/v1",
    api_key="hk-...",
    default_headers={"X-Hooshi-Identity": "off"},  # برای همهٔ درخواست‌های این کلاینت
)

# یا فقط برای یک درخواست:
client.chat.completions.create(model="openai/gpt-5.5", messages=msgs, extra_headers={"X-Hooshi-Identity": "off"})

کی خاموشش کنم؟

وقتی خودت یک persona کامل (مثلاً «دستیار فروشگاه X») تعریف کرده‌ای، یا در pipelineهای تست و ارزیابی که می‌خواهی ورودی دقیقاً همان چیزی باشد که فرستاده‌ای و حتی یک توکن اضافه هم مصرف نشود.

همین رفتار در API بومی هم برقرار است و با همان هدر خاموش می‌شود.

پایداری و Failover

هوشی برای هر پروایدر چند مسیر بالادستی دارد. اگر یک مسیر با خطای موقت (مثلاً 429 یا 5xx) مواجه شود، درخواست به‌طور خودکار و بدون هزینهٔ اضافه روی مسیر بعدی تکرار می‌شود. در حالت استریم این کار فقط تا قبل از ارسال اولین توکن ممکن است. اگر همهٔ مسیرها ناموفق باشند، خطای upstream_error می‌گیری؛ راهنمای تلاش مجدد را ببین.