ابزارها (function calling)

تعریف ابزار، حلقهٔ فراخوانی، ابزارهای آمادهٔ سمت سرور (جست‌وجو، خواندن صفحه، تاریخ) و اینکه schema ابزار هم توکن حساب می‌شود.

ابزارها (function calling) روی هر دو مسیر — chat/completions و messages — و در هر دو حالت stream و غیر-stream کار می‌کنند.

تعریف ابزار

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

resp = client.chat.completions.create(
    model="openai/gpt-4o-mini",
    messages=[{"role": "user", "content": "هوای تهران چطوره؟"}],
    tools=tools,
)

حلقهٔ فراخوانی

  1. درخواست را با tools بفرستید.
  2. اگر مدل تصمیم به فراخوانی بگیرد، finish_reason برابر tool_calls می‌شود و message.tool_calls پر می‌شود.
  3. تابع را خودتان اجرا کنید.
  4. نتیجه را با نقش tool و همان tool_call_id به تاریخچه اضافه و دوباره بفرستید.
Python
messages.append(resp.choices[0].message)          # the assistant's tool call
messages.append({
    "role": "tool",
    "tool_call_id": resp.choices[0].message.tool_calls[0].id,
    "content": '{"temp_c": 31}',
})
final = client.chat.completions.create(model=..., messages=messages, tools=tools)
schema ابزارها بخشی از پرامپت است و توکن ورودی حساب می‌شود — روی هر تماسِ همان مکالمه. ده ابزار با توضیحات طولانی می‌تواند از خودِ سؤال کاربر گران‌تر تمام شود. فقط ابزارهایی را بفرستید که در آن مرحله واقعاً ممکن است لازم شوند.

تخمین رزرو کیف پول هم schema ابزارها را حساب می‌کند، پس رزرو با ابزارهای زیاد بزرگ‌تر می‌شود.

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

سه نام در فضای نام iranrouter: را به‌جای اینکه به کد شما برگردانیم، خودمان اجرا می‌کنیم: جست‌وجو می‌کنیم یا صفحه را می‌خوانیم، نتیجه را به مدل می‌دهیم و یک پاسخ نهایی به شما می‌رسد. حلقه را شما نمی‌نویسید.

نامورودیچه می‌کند
iranrouter:web_search{"query": "...", "n": 5}جست‌وجوی وب عمومی؛ عنوان، آدرس و خلاصه برمی‌گرداند.
iranrouter:web_fetch{"url": "https://..."}یک صفحهٔ عمومی http(s) را می‌خواند و متن خواندنی‌اش را می‌دهد.
iranrouter:datetime—تاریخ و ساعت تهران و UTC، با تاریخ جلالی.
Python
resp = client.chat.completions.create(
    model="openai/gpt-4o-mini",
    messages=[{"role": "user", "content": "امروز چندم است و قیمت دلار چقدر؟"}],
    tools=[
        {"type": "function", "function": {"name": "iranrouter:datetime"}},
        {"type": "function", "function": {"name": "iranrouter:web_search"}},
    ],
)
print(resp.choices[0].message.content)   # one answer; the searching happened on our side
  • فقط همین نام‌ها مال ماست. ابزار خودتان با نام web_search همچنان مال شماست و مثل قبل به شما برمی‌گردد.
  • اگر SDK شما دونقطه را در نام ابزار نمی‌پذیرد (مثل SDK آنتروپیک)، همان ابزار را با نام iranrouter__web_search بنویسید — هر دو یکی‌اند.
  • پیشوند openrouter: و آرایهٔ plugins: [{"id": "web"}] هم پذیرفته می‌شوند: پیشوند برای هر سه ابزار، و plugins فقط به معنی web_search.
  • اگر کلاینت شما اجازهٔ افزودن ابزار نمی‌دهد (agentها و اپ‌های چت)، همان plugins: [{"id": "web"}] را به‌عنوان فیلد اضافهٔ بدنه بفرستید؛ بیشتر کلاینت‌های سازگار با OpenAI تنظیم «extra body» دارند (در Hermes: کلید extra_body در ورودی custom_providers). یا بدون هیچ تنظیمی در کلاینت: در داشبورد ← پریست‌ها «جست‌وجوی وب» را روشن کنید و مدل را @preset/شناسه بدهید. مدل هم باید function calling را پشتیبانی کند.
  • هر دور حلقه یک درخواست کامل است: رزرو، تسویه و سطر مصرف خودش را دارد. یعنی یک پاسخ ممکن است چند سطر در «مصرف» داشته باشد و مجموعشان صورت‌حساب آن درخواست است.
  • حلقه سقف دارد (پیش‌فرض ۵ دور، و در هر دور حداکثر ۵ فراخوانی ابزار). در آخرین دور مدل مجبور است با متن پاسخ بدهد، نه با درخواست ابزار تازه. هر دور در سقف RPM کلید شما هم یک درخواست شمرده می‌شود.
  • با stream: true پاسخ نهایی یک‌جا می‌رسد، نه توکن به توکن: تا مدل جواب آخر را نداده معلوم نیست آن دور آخر است. در این فاصله اتصال با heartbeat زنده می‌ماند.
  • روی این درخواست‌ها Idempotency-Key برای کل حلقه اعمال می‌شود، نه برای هر دور، و فقط بدون stream: تکرارِ حلقه‌ای که تمام شده پاسخ نهایی‌اش را از حافظه برمی‌گرداند و دوباره حساب نمی‌شود؛ اگر هنوز در حال اجراست ۴۰۹ idempotency_in_flight. با stream: true نادیده گرفته می‌شود و تکرار دوباره اجرا و حساب می‌شود. n همیشه ۱ است.
  • web_fetch فقط آدرسی را باز می‌کند که عیناً در پیام‌های شما یا در نتیجهٔ یک جست‌وجو یا fetch قبلی آمده باشد. آدرسی که مدل خودش ساخته باشد رد می‌شود — این جلوی بیرون‌کشیدن محتوای گفتگو توسط یک صفحهٔ خصمانه را می‌گیرد.
  • اگر سازمان شما guardrail محتوایی دارد، روی نتیجهٔ ابزارها هم اعمال می‌شود: دادهٔ حساس از متن صفحه حذف می‌شود و صفحه‌ای که دستور رو به مدل دارد کنار گذاشته می‌شود — بدون اینکه درخواست شما رد شود.
  • متن صفحه‌ای که خوانده می‌شود و نتیجهٔ جست‌وجو هیچ‌جا ذخیره نمی‌شود — مثل بقیهٔ محتوای درخواست.
web_fetch فقط آدرس‌های عمومی http و https روی پورت ۸۰ و ۴۴۳ را می‌خواند، بدون user:pass در آدرس. آدرس شبکهٔ داخلی و localhost رد می‌شود و هر redirect از نو بررسی می‌شود. فقط متن و HTML خوانده می‌شود، تا سقف حجم مشخص.