图像 API 的幂等键与恰好一次计费
1 分钟阅读作者 genmux API Team
每一个按量计费的 API 都有同一个噩梦场景:客户端超时、重试,然后同一份工作被扣了两次钱。或者反过来——服务商在调用中途挂掉,客户花钱什么也没拿到。本文说明我们给出的保证、它是怎么实现的,以及它能让你从自己的代码里删掉什么。
我们的保证
一笔积分最多只对应一张已交付的图片。 具体来说:
- 提交时原子地预留任务费用(一次条件式单行更新——余额永远不会变成负数,两个并发提交也不可能都花掉最后那点积分)。
- 失败或被取消的任务会自动退回预留。流水表在 (任务, 类型) 上唯一,因此退款本身也是恰好一次的,即便两个 worker 同时尝试。
- 对上游模型的调用是写前记录的:在调用供应商之前我们先记下这次尝试;如果进程在调用中途死掉,对账会拿这条记录去和供应商核对,而不是靠猜。任何状态未知的调用都不会被悄悄计费。
幂等键补上客户端那一半
如果客户端的重试会创建出第二个任务,服务端的保证就毫无意义。这正是那个必填的 Idempotency-Key 请求头存在的理由:
- 同一个 key + 同一个请求体 → 返回原来那个任务(200,不是 202)。永远不会产生新的扣费。
- 同一个 key + 不同的请求体 →
422 idempotency_key_mismatch,因为悄悄返回旧任务会把你代码里的 bug 藏起来。 - key 的记忆时长足以覆盖任何现实中的重试窗口。
实践规则:在用户意图产生的地方生成这个 key(「用户点了一次生成」对应一个 key),而不是每次 HTTP 尝试生成一个。然后你的 HTTP 层想多激进地重试都可以。
text
意图 → key K
第 1 次尝试 (K) — 网络超时,你根本没看到响应
第 2 次尝试 (K) — 200,返回第 1 次尝试创建的那个任务你可以从代码里删掉什么
- 拿你的数据库和我们对账的定时任务:退款流水在构造上就保证了
sum(扣费) == sum(已交付图片) × 单价。 - 「重试是不是把我们扣了两次?」这类告警——用同一个 key 的重试不可能做到。
- 每次提交前的防御性余额检查:
402 insufficient_credits出现时直接处理就行,不会有部分扣费的情况。
仍然需要处理的
failed任务:钱退了,但你的界面还是应该把error.code(例如content_policy)呈现出来。- 429:按
Retry-After退避;被拒绝的请求压根不会产生预留。 - 预签名 URL 过期(15 分钟):重新 GET 一次任务拿新 URL——免费。
完整行为记录在 API 参考里;如果你在对比服务商,不妨问问每一家:当一个任务在他们的队列和模型之间死掉时,你的钱会怎么样。这个问题能把值得信任的按量计费 API,和需要你每月人工审计的那些区分开来。