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