رفتن به محتوا

راهنماها

Headerها و ساختار Response

headerهایی که مهوش می‌شناسد، دو قالب response، صفحه‌بندی، شیء خطا، تکرار request و idempotency؛ همهٔ چیزهایی که یک کلاینت قابل‌اعتماد لازم دارد تا با همهٔ فراخوانی‌ها یکسان رفتار کند.

headerهای request

این headerها در همهٔ endpointهایی که آن‌ها را می‌پذیرند، معنای یکسانی دارند:

headerچه وقت بفرستیدکاربرد
Authorizationهمهٔ requestهای احرازشدهBearer <API key پروژه> یا Bearer <JWT کاربر>، بسته به نیاز endpoint
Content-Typerequestهایی که body دارندapplication/json، یا multipart/form-data که کتابخانهٔ HTTP برای آپلود می‌سازد
X-Mahvash-Organizationrequestهای JWT کاربر، وقتی در سازمانی غیر از سازمان شخصی کار می‌کنیدUUID یا slug سازمانی که عضو فعالش هستید
X-Mahvash-Projectrequestهای JWT کاربر، وقتی در پروژه‌ای غیر از پروژهٔ پیش‌فرض کار می‌کنیدUUID یا slug پروژه‌ای که به آن دسترسی دارید
X-Request-IDبرای همهٔ requestها توصیه می‌شودشناسهٔ ردگیری شما که در همهٔ پاسخ‌ها برمی‌گردد
Idempotency-Keyrequestهای پولی هوش مصنوعی که ممکن است تکرار شوندبه‌جای اجرای دوبارهٔ کار، نتیجهٔ قبلی را برمی‌گرداند
Accept: text/event-streamstream در Responses یا Chat Completionsنیت stream را صریح می‌کند؛ رفتار اصلی را stream: true تعیین می‌کند

یک request کامل با همهٔ این headerها چنین شکلی دارد:

curl https://api.mahvashai.ir/v1/responses \
  -H "Authorization: Bearer $MAHVASH_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -H "X-Request-ID: checkout-7f3a91" \
  -H "Idempotency-Key: order-1042-summary-v1" \
  -d '{"model": "auto", "input": "Summarize order 1042", "stream": true}'
  • برای body از نوع JSON، header Content-Type: application/json را بفرستید. برای آپلود فایل بگذارید کتابخانهٔ HTTP خودش multipart/form-data را بسازد و boundary را دستی تنظیم نکنید.
  • فرستادن X-Request-ID اختیاری است. اگر مقداری بین ۱ تا ۱۲۸ نویسه از A-Za-z0-9._:- بفرستید، مهوش از همان استفاده می‌کند؛ در غیر این صورت خودش یک شناسه (req_...) می‌سازد. در هر دو حالت این مقدار در header X-Request-ID پاسخ برمی‌گردد؛ چه در پاسخ موفق، چه در خطا و چه در stream. به این ترتیب لاگ‌های شما با لاگ‌های مهوش قابل تطبیق است.

دو قالب response

از روی URL می‌فهمید body را چطور بخوانید:

بخشbody پاسخ موفقbody خطافهرست‌ها
/v1/*خود شیء{ "error": { message, type, param, code } }بسته به endpoint؛ معمولاً data، first_id، last_id و has_more
/api/v1/*{ "status", "data" }{ "status", "data" } با جزئیات خطا داخل dataبسته به endpoint و داخل data؛ معمولاً با page و page_size
باینری یا 204خود بایت‌ها یا بدون bodyفقط کد وضعیتندارد
{
  "id": "resp_<uuid>",
  "object": "response",
  "status": "completed",
  "output": []
}
  • endpointهای /v1/* خود شیء را مستقیم برمی‌گردانند؛ درست همان چیزی که SDKهای OpenAI انتظار دارند.
  • endpointهای /api/v1/* داده را داخل ساختار { "status": <http status>, "data": ... } قرار می‌دهند. مقدار status همان کد وضعیت HTTP است و دادهٔ اصلی، چه در موفقیت و چه در خطا، همیشه داخل data است.
  • دانلودهای باینری (مثل محتوای فایل یا صوت ساخته‌شده) و پاسخ‌های 204 No Content اصلاً body از نوع JSON ندارند.

نکته:برای هر کدام یک تابع کمکی کوچک بنویسید؛ مثلاً unwrap() برای /api/v1 و خواندن مستقیم برای /v1. این‌طور لازم نیست برای هر فراخوانی جداگانه حدس بزنید.

صفحه‌بندی

فهرست‌ها همه یک شکل ندارند؛ پس همیشه فیلدهای صفحه‌بندی همان endpointی را بخوانید که صدا می‌زنید. دو الگوی رایج این‌هاست:

  • صفحه‌بندی با cursor در /v1: پاسخ شامل object: "list"، data، first_id، last_id و has_more است. برای صفحهٔ بعد، آخرین شناسه‌ای را که دیده‌اید در after بفرستید (یا برای صفحهٔ قبل در before) و اندازهٔ صفحه را با limit تعیین کنید.
  • صفحه‌بندی با شمارهٔ صفحه در /api/v1: پارامترهای page و page_size را می‌پذیرد و موارد فهرست و اطلاعات صفحه را داخل data برمی‌گرداند.
# First page
curl "https://api.mahvashai.ir/v1/responses/resp_<uuid>/input_items?limit=20" \
  -H "Authorization: Bearer $MAHVASH_API_KEY"

# {"object": "list", "data": [...], "first_id": "...", "last_id": "msg_x", "has_more": true}
# Next page: pass the previous last_id as "after"
curl "https://api.mahvashai.ir/v1/responses/resp_<uuid>/input_items?limit=20&after=msg_x" \
  -H "Authorization: Bearer $MAHVASH_API_KEY"

وقتی has_more برابر false شد یا صفحه‌ای خالی برگشت، متوقف شوید. اندازهٔ پیش‌فرض صفحه را ثابت فرض نکنید و شناسه‌های یک فهرست را به‌عنوان cursor در فهرست دیگری به کار نبرید.

خطاها

endpointهای /v1 خطا را با یک شیء ثابت گزارش می‌کنند که همیشه چهار کلید دارد. endpointهای /api/v1 جزئیات خطا را داخل data قرار می‌دهند:

{
  "error": {
    "message": "The selected model does not support the requested capabilities or limits.",
    "type": "invalid_request_error",
    "param": null,
    "code": "invalid_request"
  }
}

کد وضعیت HTTP نوع خطا را نشان می‌دهد:

HTTPکدهای رایجمعناچه باید کرد
400invalid_requestفیلد ناشناخته، نوع یا بازهٔ اشتباه، یا قابلیتی که مدل از آن پشتیبانی نمی‌کندrequest را اصلاح کنید؛ بدون تغییر تکرارش نکنید
401invalid_api_keytoken فرستاده نشده، منقضی یا باطل شده، یا نامعتبر استJWT را تمدید کنید یا کلید را بررسی کنید
402insufficient_balanceموجودی سازمان برای این request کافی نیستموجودی را افزایش دهید یا request کم‌هزینه‌تری بفرستید
403permission_deniedاحراز هویت شده‌اید، اما اجازهٔ این کار را نداریداز کلید یا نقشی با دسترسی لازم استفاده کنید
404not_found، response_not_foundمنبع وجود ندارد یا خارج از دسترس شماستشناسه و پروژهٔ انتخاب‌شده را بررسی کنید
409idempotency_key_reused، idempotent_request_in_progress، idempotent_stream_not_replayableتداخل در idempotency یا وضعیتبرای کار تازه کلید تازه بسازید یا نتیجه را بازیابی کنید
429rate_limit_exceeded، quota_exceeded، active_run_limitبه سقف تعداد request، سهمیه یا کارهای همزمان رسیده‌ایدکمی صبر کنید و به Retry-After توجه کنید
502provider_outcome_unknown، invalid_structured_outputارائه‌دهنده شکست خورده، قالب خواسته‌شده را رعایت نکرده، یا شاید request را پردازش کرده باشدنتیجهٔ نامعلوم را بی‌گدار تکرار نکنید؛ اول بررسی‌اش کنید
503provider_unavailable، feature_disabledهیچ مسیر مناسبی نیست یا سرویسی که لازم است در دسترس نیستبعداً دوباره تلاش کنید یا مدل و گزینه‌ها را تغییر دهید
500internal_errorخطای پیش‌بینی‌نشده در سروربا فاصلهٔ زمانی دوباره تلاش کنید و X-Request-ID را گزارش دهید

در /v1، مقدار error.type از روی کد وضعیت تعیین می‌شود و بهترین فیلد برای تصمیم‌گیری است: invalid_request_error یعنی خطا دائمی است و باید request را اصلاح کنید؛ rate_limit_error و server_error موقتی‌اند و بعد از کمی صبر می‌توانید دوباره تلاش کنید. error.code دلیل دقیق را مشخص می‌کند. این کدها ثابت‌اند، اما ممکن است کد تازه‌ای اضافه شود؛ پس همیشه یک حالت پیش‌فرض برای کدهای ناشناخته داشته باشید.

تکرار امن request

بیشتر فراخوانی‌های هوش مصنوعی هزینه دارند؛ پس سیاست تکرار باید طوری باشد که برای یک کار دو بار پول ندهید:

  • خطاهای 429 و 5xx را با فاصلهٔ زمانی رو به افزایش (exponential backoff) و کمی تصادفی‌سازی دوباره امتحان کنید. اگر Retry-After وجود داشت، دست‌کم همان تعداد ثانیه صبر کنید.
  • بقیهٔ خطاهای 4xx را بدون تغییر تکرار نکنید؛ دوباره به همان شکل شکست می‌خورند.
  • provider_outcome_unknown، timeout سمت کلاینت یا قطع اتصال پس از ارسال request را هرگز بی‌گدار تکرار نکنید. ممکن است ارائه‌دهنده کار را انجام داده باشد. پیش از هر تصمیمی، response را با شناسه‌اش بازیابی کنید یا با همان idempotency key دوباره بفرستید.
  • در همهٔ تلاش‌های مربوط به یک کار، از همان Idempotency-Key استفاده کنید. کلید تازه یعنی request تازه با هزینهٔ جداگانه.
const RETRYABLE = new Set(["rate_limit_error", "server_error"]);

export async function callMahvash(path, body, { idempotencyKey, attempts = 3 } = {}) {
  for (let attempt = 1; ; attempt++) {
    const response = await fetch("https://api.mahvashai.ir" + path, {
      method: "POST",
      headers: {
        Authorization: "Bearer " + process.env.MAHVASH_API_KEY,
        "Content-Type": "application/json",
        "Idempotency-Key": idempotencyKey, // the SAME key on every attempt
      },
      body: JSON.stringify(body),
      signal: AbortSignal.timeout(60_000),
    });
    if (response.ok) return response.json();

    const requestId = response.headers.get("X-Request-ID");
    const { error } = await response.json().catch(() => ({ error: {} }));
    const ambiguous = error?.code === "provider_outcome_unknown";
    if (!RETRYABLE.has(error?.type) || ambiguous || attempt >= attempts) {
      throw Object.assign(new Error(error?.message ?? "Mahvash error"), {
        status: response.status, code: error?.code, requestId,
      });
    }
    const retryAfter = Number(response.headers.get("Retry-After"));
    const delay = retryAfter > 0 ? retryAfter * 1000 : 500 * 2 ** attempt + Math.random() * 250;
    await new Promise((resolve) => setTimeout(resolve, delay));
  }
}

مهم:تکرار خودکار را در کتابخانه‌های HTTP و SDKها خاموش کنید (مثلاً با maxRetries: 0)، مگر مطمئن باشید همان idempotency key شما را دوباره می‌فرستند. دو لایهٔ تکرار، تعداد تلاش‌ها را چند برابر می‌کند.

کلید idempotency

Idempotency-Key پرسش «آیا request من انجام شد؟» را بی‌خطر می‌کند. endpointهای ساخت در بخش هوش مصنوعی آن را می‌پذیرند؛ یعنی Responses، Chat Completions، embedding، تصویر، صوت، moderation، search و batch. در مرجع API، این header در فهرست headerهای هر endpoint آمده است.

  • کلید باید بین ۱ تا ۲۵۵ نویسهٔ قابل‌نمایش ASCII باشد. آن را از شناسهٔ خودتان برای آن کار بسازید، مثلاً ticket-812-reply-v3، یا یک UUID که پیش از اولین تلاش ذخیره کرده‌اید.
  • هر کلید در محدودهٔ یک پروژه و یک نوع عملیات معتبر است و به‌طور پیش‌فرض ۲۴ ساعت نگه داشته می‌شود.
# First call: runs the model once and stores the result
curl https://api.mahvashai.ir/v1/responses \
  -H "Authorization: Bearer $MAHVASH_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: invoice-8812-summary" \
  -d '{"model": "auto", "input": "Summarize invoice 8812"}'

# Same key + same body within the window: the stored response is replayed,
# no second model call and no second charge.

# Same key + different body: 409 idempotency_key_reused

وقتی یک کلید دوباره فرستاده شود:

  • همان کلید و همان request، و request قبلی با موفقیت تمام شده (بدون stream): همان response ذخیره‌شده برمی‌گردد؛ بدون فراخوانی دوبارهٔ مدل و بدون هزینهٔ تازه.
  • همان کلید با body متفاوت: خطای 409 idempotency_key_reused.
  • همان کلید درحالی‌که request اول هنوز در حال اجراست: خطای 409 idempotent_request_in_progress. کمی صبر کنید و دوباره بفرستید یا response را بازیابی کنید.
  • requestهای stream هرگز دوباره پخش نمی‌شوند: خطای 409 idempotent_stream_not_replayable. به‌جای آن، response ذخیره‌شده را بازیابی کنید.
  • اگر request قبلی شکست خورده یا timeout شده باشد، کلید تا پایان اعتبارش رزرو می‌ماند؛ چون ممکن است نتیجهٔ آن نزد ارائه‌دهنده هنوز مشخص نباشد.