API 參考
防止重複扣費
冪等就是重試的請求只會執行一次:重試時帶上同一個 Idempotency-Key,即使第一次只是看起來遺失了,也只扣一次費。
逾時不等於失敗。生成可能已經在跑了,盲目重試會再開一次、也再扣一次費。帶上 Idempotency-Key,閘道只會執行一次,並在 24 小時內重放同一個回應。
#什麼時候該帶冪等鍵
建議帶上
- 所有非同步提交——影片、圖片任務、音樂、3D。這些都是花費較高的呼叫。
- 任何會被你的 worker 自動重試的非串流呼叫。
- 位於佇列之後、當機後可能重新投遞的呼叫。
不必帶
- 串流請求——該請求標頭會被 400 拒絕,串流無法重放。
- GET 輪詢和後設資料讀取——重複呼叫本來就免費且安全。
- 互動式對話:重複回答的代價比維護冪等記帳還小。
submit.sh
curl https://www.aisplice.store/v1/videos \
-H "Authorization: Bearer sk-YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"model": "doubao-seedance-2.5",
"prompt": "A drone shot of a lighthouse at dawn"
}'冪等鍵要從你自己的業務記錄派生
在重試迴圈裡現場生成 UUID 等於白做——每次嘗試都成了新身分。請從要完成的業務記錄(訂單編號、任務編號)派生,讓同一個任務的每次重試都帶同一個鍵。
#冪等鍵如何比對
必須顯式開啟,不會自動生效
不帶這個請求標頭,一切照舊。沒有冪等鍵的重試就是第二次計費請求。
只對寫方法生效
GET、HEAD、OPTIONS 會忽略該請求標頭——輪詢任務本身就可以安全重複。
作用域是 使用者 + 金鑰 + 方法 + 路由
同樣的值換一個 API 金鑰、或換一個端點,就是另一次操作。
請求主體是身分的一部分
路徑、查詢參數和請求主體會一起參與雜湊。改了負載還重複使用同一個鍵會回傳 409,而不是覆蓋。
不支援串流
串流回應無法存下來重放,因此 stream:true 加冪等鍵會被 400 拒絕。
失敗關閉
帳本不可用時請求會以 503 被拒絕,而不是在沒有保護的情況下執行。
鍵最長 255 個字元。超過長度會在請求執行前直接回傳 400。
#結果與狀態碼
被重放的回應會帶 X-Idempotent-Replayed: true 和首次的狀態碼——把它當作結果,不要當成需要對帳的重複。所有拒絕都使用扁平的守衛錯誤結構。
| HTTP | 錯誤碼 | 含義 | 處理方式 |
|---|---|---|---|
| 400 | IDEMPOTENCY_STREAM_UNSUPPORTED | 在串流請求上帶了 Idempotency-Key。 | 串流呼叫請去掉該請求標頭;需要重放保護時改用 stream:false。 |
| 400 | IDEMPOTENCY_BODY_UNREADABLE | 請求主體無法計算冪等帳本所需的雜湊。 | 請傳送常規的 JSON 或 multipart 請求主體——無法緩衝的分塊請求主體會被拒絕。 |
| 409 | IDEMPOTENCY_PAYLOAD_CONFLICT | 這個鍵已經用於另一個請求主體、路徑或查詢參數。 | 換一個新鍵。一個鍵標識的是一個確定的請求,而不是一輪重試。 |
| 409 | IDEMPOTENCY_IN_PROGRESS | 使用該鍵的首次請求仍在執行。 | 按 Retry-After(2 秒)等待後用同一個鍵重試——你會拿到首次請求的回應。 |
| 409 | IDEMPOTENCY_RESULT_UNREPLAYABLE | 首次請求已經完成,但回應被封存、無法重放。 | 不要盲目重新提交。先透過任務端點或自己的記錄對帳。 |
| 503 | IDEMPOTENCY_UNAVAILABLE | 冪等帳本不可用,守衛選擇失敗關閉,以免造成重複扣費。 | 退避後重試——這次請求並未被執行。 |
#重試迴圈
一個任務一個鍵,所有嘗試重複使用;把 409 的「處理中」當成「等待」,而不是「失敗」。
submit_once.py
import time, uuid, requests
def submit_once(job_id: str, body: dict, api_key: str, tries: int = 5):
# Same key for every attempt of this job — that is what makes it safe.
key = str(uuid.uuid5(uuid.NAMESPACE_URL, f"video-job:{job_id}"))
for attempt in range(tries):
response = requests.post(
f"https://www.aisplice.store/v1/videos",
headers={
"Authorization": f"Bearer {api_key}",
"Idempotency-Key": key,
},
json=body,
timeout=60,
)
if response.status_code == 409:
code = response.json().get("code", "")
if code == "IDEMPOTENCY_IN_PROGRESS":
time.sleep(int(response.headers.get("Retry-After", 2)))
continue
# PAYLOAD_CONFLICT or RESULT_UNREPLAYABLE: a human decision,
# never a silent resubmit.
raise RuntimeError(response.text)
if response.status_code >= 500:
time.sleep(2 ** attempt)
continue
response.raise_for_status()
replayed = response.headers.get("X-Idempotent-Replayed") == "true"
return response.json(), replayed
raise TimeoutError(f"submit for {job_id} did not settle")重試之前先把 task_id 寫入資料庫
在標記任務已提交的同一個交易裡把 task_id 寫進資料庫。冪等鍵保護的是提交動作;回應遺失之後,能救你的只有你自己的記錄。
還是沒解決?
把請求 ID 或錯誤訊息傳給我們,我們來查。