/544 字/阅读约 2 分钟

成本治理:付费 API 的计费安全与双重试状态机设计

以 OpenAI 文生图 API 为例,解析生成请求与图片下载为什么必须是两个独立重试状态机:关键差异在计费语义——一个是未确认态可能重复扣费,一个是已付费的幂等免费操作。

眠月眠月
joker1point

成本治理:付费 API 的计费安全与双重试状态机设计

是两个独立的重试状态机,而且这种分离是有意设计。


状态机一:生成请求(_request_openai_image,L1290)

python
for attempt in range(1, OPENAI_IMAGE_MAX_ATTEMPTS + 1):   # 3 次
    POST → 按错误类型分类 → retryable?
    # 退避: 5s → 15s → 30s

决策维度:错误类型决定是否可重试

  • ConnectTimeout → 可重试(请求确定未送达)
  • ReadTimeout/连接中断 → 不重试(未确认状态,防重复扣费)
  • 401/403 → 仅多 key 时重试(轮换)
  • 429/5xx → 退避重试
  • 其他 4xx → 快速失败

状态机二:图片下载(_openai_image_download_bytes,L1202)

python
for attempt in range(1, OPENAI_IMAGE_MAX_DOWNLOAD_ATTEMPTS + 1):  # 3 次
    GET image_url → 200 且有内容? → 成功返回
    # 固定退避: 2s

决策维度:只要没拿到 200 就一律重试,无错误分类、无 key 轮换、退避固定。


为什么必须是两个状态机

关键差异在计费语义,两阶段的"重试安全性"完全不同:

维度生成 POST下载 GET
是否扣费每次重发都可能扣费(未确认态)图已付费,下载是幂等的免费操作
重试安全性需按确认状态分类,保守完全安全,可无脑重试
失败后果重试错 → 重复扣费重试错 → 只是浪费几次 GET
路径关系主请求只在响应含 url 时才进入(b64_json 直接解码,跳过此状态机)

两个状态机的串联关系(_parse_openai_image_response 是衔接点):

js
POST 生成(状态机一,3次,按计费语义分类重试)
  └─ 响应含 b64_json → 直接 base64 解码(无状态机二)
  └─ 响应含 url    → 进入状态机二(3次,幂等重试同一 URL)

总结

这正是 review 修复后统一的计费安全原则的体现:已扣费的内容(生成结果 URL)失败就重试原地址拿回,绝不重新发起付费请求;而付费请求本身(POST)只在没有扣费确定性的前提下才允许重试。

同一个"幂等性"原则,在两个状态机上的具体策略截然相反——一个激进重试,一个保守分类。面试讲这段时,这个对比是最好的展开点。