service_tier_billing.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. package service
  2. import (
  3. "log/slog"
  4. "strings"
  5. )
  6. // ServiceTierBillingResolution describes how the billable service tier of one
  7. // request was settled between the tier the client asked for and the tier the
  8. // upstream reports having used.
  9. type ServiceTierBillingResolution struct {
  10. Requested string // tier carried by the request sent upstream ("" when none)
  11. Observed string // tier declared by the upstream response ("" when none)
  12. Billing string // tier used for billing and the usage log
  13. Downgraded bool // Billing is cheaper than Requested
  14. }
  15. // ResolveBillingServiceTier picks the tier to bill. The upstream declaration is
  16. // trusted only to lower the bill: a request served below the tier it asked for
  17. // (OpenAI "priority" answered with service_tier "default", Anthropic speed=fast
  18. // answered with usage.speed "standard") is billed at the cheaper tier. A response
  19. // that claims a more expensive tier, an unknown one, or none at all leaves the
  20. // requested tier in place, which is also the behaviour for upstreams that never
  21. // declare a tier.
  22. func ResolveBillingServiceTier(requested, observed string) ServiceTierBillingResolution {
  23. requested = normalizeBillingServiceTier(requested)
  24. observed = normalizeBillingServiceTier(observed)
  25. resolution := ServiceTierBillingResolution{Requested: requested, Observed: observed, Billing: requested}
  26. if observed == "" || observed == requested {
  27. return resolution
  28. }
  29. observedRank, known := serviceTierCostRank(observed)
  30. if !known {
  31. return resolution
  32. }
  33. requestedRank, _ := serviceTierCostRank(requested)
  34. if observedRank >= requestedRank {
  35. return resolution
  36. }
  37. resolution.Billing = observed
  38. resolution.Downgraded = true
  39. return resolution
  40. }
  41. // serviceTierCostRank orders tiers by their cost relative to the base rate, so a
  42. // lower rank is always cheaper. Unknown tiers rank as the base rate and report
  43. // known=false so callers can refuse to act on them.
  44. func serviceTierCostRank(tier string) (rank int, known bool) {
  45. switch normalizeBillingServiceTier(tier) {
  46. case "flex":
  47. return 0, true
  48. case "", "default", "standard", "auto", "scale":
  49. return 1, true
  50. case "priority", "fast":
  51. return 2, true
  52. default:
  53. return 1, false
  54. }
  55. }
  56. // ResolveOpenAIServiceTierBilling applies the response-tier contract for the
  57. // selected credential. Public OpenAI API responses declare the actual tier and
  58. // may lower billing. The private ChatGPT Codex endpoint does not: it commonly
  59. // reports default for effective Fast turns, so OAuth-like credentials retain
  60. // the final outbound tier while still exposing the observed value.
  61. func ResolveOpenAIServiceTierBilling(account *Account, requested, observed string) ServiceTierBillingResolution {
  62. if account != nil && account.IsOpenAIOAuthLike() && codexOAuthResponseTierIsNonAuthoritative(observed) {
  63. return ServiceTierBillingResolution{
  64. Requested: normalizeBillingServiceTier(requested),
  65. Observed: normalizeBillingServiceTier(observed),
  66. Billing: normalizeBillingServiceTier(requested),
  67. }
  68. }
  69. return ResolveBillingServiceTier(requested, observed)
  70. }
  71. func codexOAuthResponseTierIsNonAuthoritative(observed string) bool {
  72. switch normalizeBillingServiceTier(observed) {
  73. case "default":
  74. return true
  75. default:
  76. return false
  77. }
  78. }
  79. // ApplyOpenAIServiceTierBillingResolution lowers result.ServiceTier only when
  80. // the selected credential's upstream response tier is authoritative.
  81. func ApplyOpenAIServiceTierBillingResolution(account *Account, result *OpenAIForwardResult) ServiceTierBillingResolution {
  82. if result == nil {
  83. return ServiceTierBillingResolution{}
  84. }
  85. resolution := ResolveOpenAIServiceTierBilling(account, optionalStringValue(result.ServiceTier), result.UpstreamResponseServiceTier)
  86. if resolution.Downgraded {
  87. billing := resolution.Billing
  88. result.ServiceTier = &billing
  89. }
  90. return resolution
  91. }
  92. // ApplyForwardServiceTierBillingResolution is the ForwardResult counterpart of
  93. // ApplyOpenAIServiceTierBillingResolution.
  94. func ApplyForwardServiceTierBillingResolution(result *ForwardResult) ServiceTierBillingResolution {
  95. if result == nil {
  96. return ServiceTierBillingResolution{}
  97. }
  98. resolution := ResolveBillingServiceTier(optionalStringValue(result.ServiceTier), result.UpstreamResponseServiceTier)
  99. if resolution.Downgraded {
  100. billing := resolution.Billing
  101. result.ServiceTier = &billing
  102. }
  103. return resolution
  104. }
  105. // logServiceTierBillingDowngrade leaves an audit trail for every request billed
  106. // below the tier it asked for; unchanged tiers are not logged.
  107. func logServiceTierBillingDowngrade(component string, account *Account, requestID string, resolution ServiceTierBillingResolution) {
  108. if !resolution.Downgraded {
  109. return
  110. }
  111. attrs := []any{
  112. "component", component,
  113. "request_id", strings.TrimSpace(requestID),
  114. "requested_tier", resolution.Requested,
  115. "response_tier", resolution.Observed,
  116. "billed_tier", resolution.Billing,
  117. }
  118. if account != nil {
  119. attrs = append(attrs, "platform", account.Platform, "account_id", account.ID)
  120. }
  121. slog.Info("billing.service_tier_downgraded", attrs...)
  122. }