channel_pricing_multipliers_test.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. package service
  2. import (
  3. "testing"
  4. "github.com/stretchr/testify/require"
  5. )
  6. func pricingMultiplier(value float64) *float64 { return &value }
  7. func TestConfiguredServiceTierMultiplier(t *testing.T) {
  8. tests := []struct {
  9. name string
  10. serviceTier string
  11. pricing *ModelPricing
  12. want float64
  13. }{
  14. {name: "gpt-5.5 fast", serviceTier: "fast", pricing: &ModelPricing{FastMultiplier: pricingMultiplier(2.5)}, want: 2.5},
  15. {name: "priority alias", serviceTier: "priority", pricing: &ModelPricing{FastMultiplier: pricingMultiplier(2)}, want: 2},
  16. {name: "flex configured", serviceTier: "flex", pricing: &ModelPricing{FlexMultiplier: pricingMultiplier(0.4)}, want: 0.4},
  17. {name: "legacy fast default", serviceTier: "fast", pricing: &ModelPricing{}, want: 2},
  18. {name: "ultrafast default", serviceTier: "ultrafast", pricing: &ModelPricing{}, want: 2},
  19. {name: "ultrafast ignores fast multiplier", serviceTier: "ultrafast", pricing: &ModelPricing{FastMultiplier: pricingMultiplier(2.5)}, want: 2},
  20. {name: "legacy flex default", serviceTier: "flex", pricing: &ModelPricing{}, want: 0.5},
  21. }
  22. for _, tt := range tests {
  23. t.Run(tt.name, func(t *testing.T) {
  24. require.InDelta(t, tt.want, configuredServiceTierMultiplier(tt.serviceTier, tt.pricing), 1e-12)
  25. })
  26. }
  27. }
  28. func TestConfiguredServiceTierMultiplierAppliesToEveryTokenComponent(t *testing.T) {
  29. pricing := &ModelPricing{
  30. InputPricePerToken: 5e-6,
  31. OutputPricePerToken: 30e-6,
  32. CacheCreationPricePerToken: 6.25e-6,
  33. CacheReadPricePerToken: 0.5e-6,
  34. FastMultiplier: pricingMultiplier(2.5),
  35. FlexMultiplier: pricingMultiplier(0.5),
  36. }
  37. tokens := UsageTokens{
  38. InputTokens: 1_000_000,
  39. OutputTokens: 1_000_000,
  40. CacheCreationTokens: 1_000_000,
  41. CacheReadTokens: 1_000_000,
  42. }
  43. service := &BillingService{}
  44. fast := service.computeTokenBreakdown(pricing, tokens, 1, "fast", false)
  45. require.InDelta(t, 12.5, fast.InputCost, 1e-12)
  46. require.InDelta(t, 75, fast.OutputCost, 1e-12)
  47. require.InDelta(t, 15.625, fast.CacheCreationCost, 1e-12)
  48. require.InDelta(t, 1.25, fast.CacheReadCost, 1e-12)
  49. flex := service.computeTokenBreakdown(pricing, tokens, 1, "flex", false)
  50. require.InDelta(t, 2.5, flex.InputCost, 1e-12)
  51. require.InDelta(t, 15, flex.OutputCost, 1e-12)
  52. require.InDelta(t, 3.125, flex.CacheCreationCost, 1e-12)
  53. require.InDelta(t, 0.25, flex.CacheReadCost, 1e-12)
  54. }
  55. func TestChannelOverridePreservesCatalogFastRatioByDefault(t *testing.T) {
  56. pricing := &ModelPricing{
  57. InputPricePerToken: 2,
  58. InputPricePerTokenPriority: 4,
  59. OutputPricePerToken: 6,
  60. OutputPricePerTokenPriority: 12,
  61. }
  62. applyChannelTokenPriceOverrides(pricing, &ChannelModelPricing{
  63. InputPrice: pricingMultiplier(3),
  64. OutputPrice: pricingMultiplier(9),
  65. })
  66. require.InDelta(t, 3, pricing.InputPricePerToken, 1e-12)
  67. require.InDelta(t, 6, pricing.InputPricePerTokenPriority, 1e-12)
  68. require.InDelta(t, 9, pricing.OutputPricePerToken, 1e-12)
  69. require.InDelta(t, 18, pricing.OutputPricePerTokenPriority, 1e-12)
  70. }
  71. func TestAnthropicFastUsesDefaultMultiplierWithoutCatalogTier(t *testing.T) {
  72. pricing := &ModelPricing{InputPricePerToken: 5e-6, OutputPricePerToken: 25e-6}
  73. cost := (&BillingService{}).computeTokenBreakdown(pricing, UsageTokens{
  74. InputTokens: 1_000_000, OutputTokens: 1_000_000,
  75. }, 1, "fast", false)
  76. require.InDelta(t, 10, cost.InputCost, 1e-12)
  77. require.InDelta(t, 50, cost.OutputCost, 1e-12)
  78. }
  79. func TestBuiltInModelFastDefaults(t *testing.T) {
  80. service := &BillingService{fallbackPrices: make(map[string]*ModelPricing)}
  81. service.initFallbackPricing()
  82. for _, tt := range []struct {
  83. model string
  84. want float64
  85. }{
  86. {model: "gpt-5.5", want: 2.5},
  87. {model: "claude-opus-4.8", want: 2},
  88. {model: "claude-opus-5", want: 2},
  89. } {
  90. pricing := service.fallbackPrices[tt.model]
  91. require.NotNil(t, pricing)
  92. require.InDelta(t, tt.want, pricing.InputPricePerTokenPriority/pricing.InputPricePerToken, 1e-12)
  93. require.InDelta(t, tt.want, pricing.OutputPricePerTokenPriority/pricing.OutputPricePerToken, 1e-12)
  94. }
  95. }
  96. func TestIntervalMultipliersApplyToChannelBase(t *testing.T) {
  97. base := &ModelPricing{
  98. InputPricePerToken: 5,
  99. OutputPricePerToken: 30,
  100. CacheCreationPricePerToken: 6.25,
  101. CacheCreation5mPrice: 6.25,
  102. CacheCreation1hPrice: 6.25,
  103. CacheReadPricePerToken: 0.5,
  104. FastMultiplier: pricingMultiplier(2),
  105. FlexMultiplier: pricingMultiplier(0.5),
  106. }
  107. resolved := &ResolvedPricing{
  108. BasePricing: base,
  109. Intervals: []PricingInterval{{
  110. MinTokens: 272000,
  111. InputMultiplier: pricingMultiplier(2),
  112. OutputMultiplier: pricingMultiplier(1.5),
  113. CacheWriteMultiplier: pricingMultiplier(2),
  114. CacheReadMultiplier: pricingMultiplier(2),
  115. }},
  116. }
  117. pricing := (&ModelPricingResolver{}).GetIntervalPricing(resolved, 272001)
  118. require.InDelta(t, 10, pricing.InputPricePerToken, 1e-12)
  119. require.InDelta(t, 45, pricing.OutputPricePerToken, 1e-12)
  120. require.InDelta(t, 12.5, pricing.CacheCreationPricePerToken, 1e-12)
  121. require.InDelta(t, 1, pricing.CacheReadPricePerToken, 1e-12)
  122. require.Same(t, base, (&ModelPricingResolver{}).GetIntervalPricing(resolved, 272000))
  123. }
  124. func TestIntervalExplicitPriceTakesPrecedenceOverMultiplier(t *testing.T) {
  125. pricing := intervalToModelPricing(&PricingInterval{
  126. InputPrice: pricingMultiplier(7),
  127. InputMultiplier: pricingMultiplier(2),
  128. }, &ModelPricing{InputPricePerToken: 5}, nil)
  129. require.InDelta(t, 7, pricing.InputPricePerToken, 1e-12)
  130. }
  131. func TestIntervalPricePreservesDefaultFastRatio(t *testing.T) {
  132. pricing := intervalToModelPricing(&PricingInterval{
  133. InputPrice: pricingMultiplier(7),
  134. }, &ModelPricing{
  135. InputPricePerToken: 5,
  136. InputPricePerTokenPriority: 10,
  137. }, nil)
  138. require.InDelta(t, 7, pricing.InputPricePerToken, 1e-12)
  139. require.InDelta(t, 14, pricing.InputPricePerTokenPriority, 1e-12)
  140. }
  141. func TestAnthropicSpeedServiceTier(t *testing.T) {
  142. account := &Account{Platform: PlatformAnthropic}
  143. for _, model := range []string{"claude-opus-5", "claude-opus-4-8", "claude-opus-4.8"} {
  144. tier := anthropicSpeedServiceTier(account, "fast", model)
  145. require.NotNil(t, tier, "model %s should bill as fast", model)
  146. require.Equal(t, "fast", *tier)
  147. }
  148. require.Nil(t, anthropicSpeedServiceTier(&Account{Platform: PlatformOpenAI}, "fast", "claude-opus-5"))
  149. require.Nil(t, anthropicSpeedServiceTier(account, "standard", "claude-opus-5"))
  150. }
  151. // fast mode 不存在于这些模型/承载上,即便客户端传了 speed=fast 也不能计 2x。
  152. func TestAnthropicSpeedServiceTierRejectsUnsupportedTargets(t *testing.T) {
  153. account := &Account{Platform: PlatformAnthropic}
  154. for _, model := range []string{
  155. "claude-opus-4-7", // fast mode 已被移除
  156. "claude-opus-4-6", //
  157. "claude-opus-4-5", // 不能被 "opus-5" 规则误判
  158. "claude-sonnet-5", // 非 Opus
  159. "claude-haiku-4-5", //
  160. "", //
  161. } {
  162. require.Nil(t, anthropicSpeedServiceTier(account, "fast", model),
  163. "model %q must not bill as fast", model)
  164. }
  165. bedrock := &Account{Platform: PlatformAnthropic, Type: AccountTypeBedrock}
  166. require.Nil(t, anthropicSpeedServiceTier(bedrock, "fast", "claude-opus-5"))
  167. }
  168. func TestAnthropicSpeedModelPrefersMappedUpstreamModel(t *testing.T) {
  169. parsed := &ParsedRequest{Model: "claude-opus-5"}
  170. require.Equal(t, "claude-opus-4-7", anthropicSpeedModel(parsed, &ForwardResult{
  171. UpstreamModel: "claude-opus-4-7",
  172. }))
  173. require.Equal(t, "claude-opus-5", anthropicSpeedModel(parsed, &ForwardResult{}))
  174. }
  175. func TestMultiplierOnlyIntervalIsValid(t *testing.T) {
  176. require.NoError(t, ValidateIntervals([]PricingInterval{{
  177. MinTokens: 199999,
  178. InputMultiplier: pricingMultiplier(2),
  179. }}, BillingModeToken))
  180. require.NoError(t, checkIntervalsHavePrices(ChannelModelPricing{
  181. Models: []string{"grok-4.6"},
  182. Intervals: []PricingInterval{{
  183. MinTokens: 199999,
  184. InputMultiplier: pricingMultiplier(2),
  185. }},
  186. }))
  187. }
  188. func TestChannelMultipliersMustBePositive(t *testing.T) {
  189. zero := 0.0
  190. require.Error(t, checkPricesNotNegative(ChannelModelPricing{FastMultiplier: &zero}))
  191. require.Error(t, checkPricesNotNegative(ChannelModelPricing{FlexMultiplier: &zero}))
  192. require.Error(t, ValidateIntervals([]PricingInterval{{
  193. MinTokens: 100,
  194. InputMultiplier: &zero,
  195. }}, BillingModeToken))
  196. }
  197. func TestCalculateTokenCostContextTierEnablement(t *testing.T) {
  198. base := &ModelPricing{InputPricePerToken: 1e-6}
  199. resolved := &ResolvedPricing{
  200. BasePricing: base,
  201. Intervals: []PricingInterval{{
  202. MinTokens: 100,
  203. InputMultiplier: pricingMultiplier(2),
  204. }},
  205. }
  206. resolver := &ModelPricingResolver{}
  207. service := &BillingService{}
  208. tokens := UsageTokens{InputTokens: 200}
  209. t.Run("group disabled uses base tier", func(t *testing.T) {
  210. resolved.longContextPricingEnabled = false
  211. cost, err := service.calculateTokenCost(resolved, CostInput{
  212. Model: "custom", Tokens: tokens, RateMultiplier: 1, Resolver: resolver,
  213. })
  214. require.NoError(t, err)
  215. require.InDelta(t, 200e-6, cost.TotalCost, 1e-12)
  216. })
  217. t.Run("group enabled uses interval", func(t *testing.T) {
  218. resolved.longContextPricingEnabled = true
  219. accountDisabled := false
  220. cost, err := service.calculateTokenCost(resolved, CostInput{
  221. Model: "custom", Tokens: tokens, RateMultiplier: 1, Resolver: resolver,
  222. LongContextBillingEnabled: &accountDisabled,
  223. })
  224. require.NoError(t, err)
  225. require.InDelta(t, 400e-6, cost.TotalCost, 1e-12)
  226. })
  227. t.Run("account enabled overrides disabled group", func(t *testing.T) {
  228. resolved.longContextPricingEnabled = false
  229. accountEnabled := true
  230. cost, err := service.calculateTokenCost(resolved, CostInput{
  231. Model: "custom", Tokens: tokens, RateMultiplier: 1, Resolver: resolver,
  232. LongContextBillingEnabled: &accountEnabled,
  233. })
  234. require.NoError(t, err)
  235. require.InDelta(t, 400e-6, cost.TotalCost, 1e-12)
  236. })
  237. }
  238. func TestCalculateTokenCostCombinesIntervalAndFastMultiplier(t *testing.T) {
  239. resolved := &ResolvedPricing{
  240. BasePricing: &ModelPricing{
  241. InputPricePerToken: 1e-6,
  242. FastMultiplier: pricingMultiplier(2.5),
  243. },
  244. Intervals: []PricingInterval{{
  245. MinTokens: 100,
  246. InputMultiplier: pricingMultiplier(2),
  247. }},
  248. longContextPricingEnabled: true,
  249. }
  250. cost, err := (&BillingService{}).calculateTokenCost(resolved, CostInput{
  251. Model: "custom", Tokens: UsageTokens{InputTokens: 200}, RateMultiplier: 1,
  252. ServiceTier: "fast", Resolver: &ModelPricingResolver{},
  253. })
  254. require.NoError(t, err)
  255. require.InDelta(t, 1e-3, cost.TotalCost, 1e-12)
  256. }