رفتن به محتوا

کلید یکتاسازی (Idempotency)

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

مستندات

چرا لازم است

هنگام تلاش مجدد یک درخواست (به‌خاطر تایم‌اوت یا قطعی شبکه) ممکن است مدل دو بار صدا زده شود و دو بار هم هزینه کسر گردد. برای جلوگیری، هدر Idempotency-Key را با یک شناسهٔ یکتا (مثلاً UUID) به درخواست اضافه کنید (سازگار با OpenAI).

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

کلید یکتاسازی (Idempotency)، جلوگیری از کسر هزینهٔ دوباره هنگام Retry

بدون این هدر، ارسال دوبارهٔ یک درخواست، دو بار پردازش و دو بار هزینه دارد.

نمونهٔ کد

هر تب همان درخواست را دو بار می‌فرستد، با یک کلید یکسان، و هر دو شناسهٔ پاسخ را چاپ می‌کند. چیزی که باید ببینید این است: دو شناسهٔ برابر، دو هزینهٔ برابر، و Idempotent-Replayed روی پاسخ دوم.

# یک کلید برای کل عملیات. بش ابزار ساخت UUID ندارد، پس کلید از ساعت و شناسهٔ# فرایند ساخته می‌شود؛ در کد واقعی uuid4 بسازید.IDEMPOTENCY_KEY="$(date +%s)-$" # دو بار ارسال با همان کلید: باید دو id یکسان ببینید و یک Idempotent-Replayed.for attempt in 1 2; do  curl -s -i https://api-ai.hibanacloud.ir/v1/chat/completions \    -H "Authorization: Bearer YOUR_API_KEY" \    -H "Content-Type: application/json" \    -H "Idempotency-Key: $IDEMPOTENCY_KEY" \    -d '{      "model": "gpt-5-nano",      "messages": [{"role": "user", "content": "سلام! یک جملهٔ کوتاه بگو."}],      "max_completion_tokens": 4096    }' | grep -o -i -E '"id":"[^"]*"|Idempotent-Replayed: true'done

انتخاب کلید

کلید فقط باید یکتا باشد؛ شکل خاصی برایش الزامی نیست. آنچه اهمیت دارد این است که کلید به عملیات بچسبد، نه به تلاش HTTP:

  • کلید را پیش از اولین ارسال بسازید و تا پایان کار نگه دارید. اگر کلید را داخل تابعِ ارسال بسازید، هر تلاش مجدد کلید تازه‌ای می‌گیرد و یکتاسازی هیچ کاری نمی‌کند.
  • uuid4 ساده‌ترین انتخاب است، اما شناسهٔ خودتان هم کار می‌کند (شناسهٔ ردیف صف، شناسهٔ پیام کاربر، یا شناسهٔ کار در پایگاه‌داده) به شرطی که برای دو عملیات متفاوت تکرار نشود.
  • کلید را برای درخواستی با بدنهٔ متفاوت دوباره به کار نبرید. این یک انضباط سمت کلاینت است: کلید یعنی «همان عملیات»، و اگر پیام‌ها یا مدل عوض شده باشند دیگر همان عملیات نیست.
  • کلید در هدر می‌رود و ممکن است در لاگ‌ها بماند، پس دادهٔ حساس (شمارهٔ کارت، توکن، متن پیام کاربر) را داخل آن نگذارید.
  • اگر عملیات را از چند سرور یا چند worker می‌فرستید، کلید را همراه خود کار ذخیره کنید، نه در حافظهٔ فرایند، وگرنه تلاش مجدد روی worker دیگر کلید دیگری خواهد داشت.

چرخهٔ عمر کلید

  • اولین ارسال عادی پردازش می‌شود و پاسخش تا ۲۴ ساعت ذخیره می‌گردد.
  • ارسال مجدد پس از اتمام درخواست اول، همان پاسخ ذخیره‌شده را برمی‌گرداند، بدون صدا زدن دوبارهٔ مدل و بدون کسر هزینهٔ دوباره. در این حالت پاسخ، هدر Idempotent-Replayed: true دارد.
  • اگر همان درخواست را بار دوم در حالی که درخواست اول هنوز در حال پردازش است بفرستید، پاسخ 409 با کد idempotency_key_in_use برمی‌گردد؛ پس از اتمام درخواست اول دوباره تلاش کنید تا پاسخ ذخیره‌شده را بگیرید.
نتیجهٔ ارسال دوم با همان کلید
وضعیت درخواست اولنتیجهٔ ارسال دومکاری که باید بکنید
هنوز در حال پردازش است409 با کد idempotency_key_in_useکمی صبر کنید و با همان کلید دوباره بفرستید.
تمام شده، کمتر از ۲۴ ساعت پیشهمان پاسخ ذخیره‌شده، به همراه Idempotent-Replayed: trueکاری لازم نیست؛ مدل دوباره صدا زده نشده و هزینه‌ای کسر نشده.
بیش از ۲۴ ساعت پیشفرض کنید پاسخ ذخیره‌شده دیگر در دسترس نیست.اگر عملیات باید دوباره انجام شود، یک کلید تازه بسازید.

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

ترکیب با محدودیت نرخ (۴۲۹)

محدودیت نرخ و یکتاسازی دقیقاً در یک نقطه به هم می‌رسند: لحظه‌ای که پس از 429 تصمیم می‌گیرید دوباره بفرستید. اگر آن تلاش مجدد همان Idempotency-Key را ببرد، دیگر مهم نیست درخواست قبلی پردازش شده بود یا نه، نتیجه یا همان کار است یا همان پاسخ. اگر کلید تازه‌ای ببرد، تلاش مجدد یک عملیات جدید است و می‌تواند هزینهٔ جدید داشته باشد.

  1. کلید را پیش از اولین ارسال بسازید.
  2. با دیدن 429 به اندازهٔ Retry-After صبر کنید (و اگر آن هدر نبود، exponential backoff).
  3. همان کلید را دوباره بفرستید، کلید تازه نسازید.
  4. با دیدن 409 کمی عقب‌نشینی کنید و باز هم همان کلید را بفرستید؛ این یعنی تلاش قبلی هنوز در جریان است.
  5. پس از موفقیت، Idempotent-Replayed را نگاه کنید تا بدانید پاسخ تازه بود یا بازپخش.
import timeimport uuidimport requests BASE = "https://api-ai.hibanacloud.ir"API_KEY = "HIBANA_API_KEY" payload = {    "model": "deepseek-chat",    "messages": [{"role": "user", "content": "سلام"}],} # یک کلید برای کل عملیات، نه یکی برای هر تلاشidem_key = str(uuid.uuid4()) for attempt in range(5):    response = requests.post(        f"{BASE}/v1/chat/completions",        headers={            "Authorization": f"Bearer {API_KEY}",            "Idempotency-Key": idem_key,        },        json=payload,        timeout=60,    )     if response.status_code == 429:        # محدودیت نرخ. صبر کنید و با همان کلید دوباره بفرستید.        wait = int(response.headers.get("Retry-After", 60))        print(f"محدودیت نرخ. صبر {wait} ثانیه...")        time.sleep(wait)        continue     if response.status_code == 409:        # idempotency_key_in_use، تلاش قبلی هنوز در حال پردازش است.        time.sleep(2 ** attempt)        continue     if response.headers.get("Idempotent-Replayed") == "true":        print("پاسخ تکراری بود؛ هزینهٔ دوباره کسر نشد.")     response.raise_for_status()    break

محدودیت‌ها

  • کلید برای هر کلید API جداگانه است (حداکثر ۲۵۵ کاراکتر) و فقط برای پاسخ‌های غیر استریم فعال است.
  • برای پاسخ‌های استریم این هدر کاری نمی‌کند. اگر تلاش مجدد امن برایتان مهم است، همان درخواست را غیر استریم بفرستید، یا در سمت خودتان جلوی ارسال دوباره را بگیرید.
  • چون کلید به کلید API گره خورده است، دو سرویس با دو کلید API متفاوت فضای کلید مشترکی ندارند؛ یک کلید یکتاسازی مشترک میان آن‌ها معنایی ندارد.