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 或报错信息发给我们,我们来查。