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錯誤碼含義處理方式
400IDEMPOTENCY_STREAM_UNSUPPORTED在串流請求上帶了 Idempotency-Key。串流呼叫請去掉該請求標頭;需要重放保護時改用 stream:false。
400IDEMPOTENCY_BODY_UNREADABLE請求主體無法計算冪等帳本所需的雜湊。請傳送常規的 JSON 或 multipart 請求主體——無法緩衝的分塊請求主體會被拒絕。
409IDEMPOTENCY_PAYLOAD_CONFLICT這個鍵已經用於另一個請求主體、路徑或查詢參數。換一個新鍵。一個鍵標識的是一個確定的請求,而不是一輪重試。
409IDEMPOTENCY_IN_PROGRESS使用該鍵的首次請求仍在執行。按 Retry-After(2 秒)等待後用同一個鍵重試——你會拿到首次請求的回應。
409IDEMPOTENCY_RESULT_UNREPLAYABLE首次請求已經完成,但回應被封存、無法重放。不要盲目重新提交。先透過任務端點或自己的記錄對帳。
503IDEMPOTENCY_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 或錯誤訊息傳給我們,我們來查。