fix(billing): hold inflight reservation until billing lands; price estimate like billing; wire remaining endpoints

Address gaps found in adversarial verification of the in-flight balance
reservation:

- Early release: the reservation is now a ref-counted handle carried on the
  request context. Billing tasks submitted via submit*UsageRecordTask take a
  ref and return it only after RecordUsage has run; with reservation enabled
  the balance cache is deducted synchronously inside the billing task, so the
  reservation is released only after admission can see the deduction. Dropped
  tasks return their ref immediately. The reservation is renewed every ttl/3
  while the handler runs; after the handler returns it is no longer renewed,
  so the post-handler hold is bounded by one TTL.
- Estimate: use the same billing model (channel mapping + BillingModelSource,
  max over requested/mapped when ambiguous) and ModelPricingResolver (group
  ModelPricing incl. wildcards, channel per_request/image/video/token
  overrides), default/group/user rate + peak multiplier, plus image, video,
  audio and search surcharge kinds. Unpriced requests fail open with a
  throttled log; new billing.inflight_reservation.fail_closed_on_unpriced
  (default false) rejects them instead.
- Wire images, embeddings, web/x search, alpha search, grok media generation,
  grok realtime/voice, OpenAI-platform /v1/messages and ResponsesWebSocket
  (session-level reservation with renewal).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
William Drake
2026-09-28 16:14:43 -04:00
co-authored by Claude Opus 5.5
parent 43f88c99aa
commit 52c123e6f4
21 changed files with 1250 additions and 102 deletions
+9 -2
View File
@@ -1135,8 +1135,11 @@ billing:
# 用户无在途请求时始终放行(与旧行为一致);估算失败/Redis 不可用时 fail-open。
inflight_reservation:
enabled: true
# Max lifetime of a single reservation (self-heals leaked reservations)
# 单条预留最长存活时间(秒),泄漏的预留到期自动释放
# Reservation TTL. Renewed every ttl/3 while the request is running; after the
# handler returns it is held (not renewed) until the async billing task has
# deducted the cached balance, so a stuck billing task frees it after at most one TTL.
# 预留 TTL(秒)。请求进行中每 ttl/3 续期;请求结束后不再续期,保持到异步计费
# 扣减余额缓存后释放,计费任务卡死时最多一个 TTL 后自动失效
ttl_seconds: 900
# Output tokens assumed when the request has no max_tokens
# 请求未携带 max_tokens 时假定的输出 token 数
@@ -1148,6 +1151,10 @@ billing:
# Cap for a single reservation in USD (0 = no cap)
# 单请求预留金额上限(USD,0 为不设上限)
max_reservation_usd: 0
# When a request cannot be priced (no base / group / channel pricing for the
# billing model), reject it instead of admitting it without reservation.
# 无法为请求定价(计费模型无基础/分组/渠道定价)时拒绝请求;默认 false 为放行不预留(节流告警日志)
fail_closed_on_unpriced: false
# =============================================================================
# Turnstile Configuration