کلید یکتاسازی (Idempotency)
جلوگیری از پردازش و کسر هزینهٔ دوباره وقتی یک درخواست را دوباره میفرستید
مستندات
چرا لازم است
هنگام تلاش مجدد یک درخواست (بهخاطر تایماوت یا قطعی شبکه) ممکن است مدل دو بار صدا زده شود و دو بار هم هزینه کسر گردد. برای جلوگیری، هدر Idempotency-Key را با یک شناسهٔ یکتا (مثلاً UUID) به درخواست اضافه کنید (سازگار با OpenAI).
ریشهٔ مشکل این است که یک تایماوت از سمت کلاینت مبهم است: نمیدانید درخواست اصلاً به سرور نرسیده، یا رسیده و پردازش شده و فقط پاسخش در راه برگشت گم شده است. بدون کلید یکتا، تنها دو انتخاب دارید، دوباره بفرستید و ریسک هزینهٔ دوباره را بپذیرید، یا نفرستید و ریسک انجامنشدن کار را. کلید یکتاسازی این انتخاب را حذف میکند: تلاش دوم یا همان کار است یا همان پاسخ.
کلید یکتاسازی (Idempotency)، جلوگیری از کسر هزینهٔ دوباره هنگام Retry
بدون این هدر، ارسال دوبارهٔ یک درخواست، دو بار پردازش و دو بار هزینه دارد.
هدر Idempotency-Key
تنها هدری که شما میفرستید Idempotency-Key است. هدر Idempotent-Replayed را سرور در پاسخ قرار میدهد تا بفهمید پاسخ تکراری است یا نه؛ شما آن را تنظیم نمیکنید و فرستادنش هیچ اثری ندارد.
| هدر | جهت | معنی |
|---|---|---|
| Idempotency-Key | در درخواست، شما میفرستید | شناسهٔ یکتای این عملیات. در همهٔ تلاشهای همان عملیات یکسان بماند. |
| Idempotent-Replayed | در پاسخ، سرور میفرستد | اگر true باشد، این پاسخ همان پاسخ ذخیرهشده است و هزینهٔ تازهای نداشته. |
نمونهٔ کد
هر تب همان درخواست را دو بار میفرستد، با یک کلید یکسان، و هر دو شناسهٔ پاسخ را چاپ میکند. چیزی که باید ببینید این است: دو شناسهٔ برابر، دو هزینهٔ برابر، و 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 را ببرد، دیگر مهم نیست درخواست قبلی پردازش شده بود یا نه، نتیجه یا همان کار است یا همان پاسخ. اگر کلید تازهای ببرد، تلاش مجدد یک عملیات جدید است و میتواند هزینهٔ جدید داشته باشد.
- کلید را پیش از اولین ارسال بسازید.
- با دیدن
429به اندازهٔRetry-Afterصبر کنید (و اگر آن هدر نبود، exponential backoff). - همان کلید را دوباره بفرستید، کلید تازه نسازید.
- با دیدن
409کمی عقبنشینی کنید و باز هم همان کلید را بفرستید؛ این یعنی تلاش قبلی هنوز در جریان است. - پس از موفقیت،
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 متفاوت فضای کلید مشترکی ندارند؛ یک کلید یکتاسازی مشترک میان آنها معنایی ندارد.