| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290 |
- package service
- import (
- "testing"
- "github.com/stretchr/testify/require"
- )
- func pricingMultiplier(value float64) *float64 { return &value }
- func TestConfiguredServiceTierMultiplier(t *testing.T) {
- tests := []struct {
- name string
- serviceTier string
- pricing *ModelPricing
- want float64
- }{
- {name: "gpt-5.5 fast", serviceTier: "fast", pricing: &ModelPricing{FastMultiplier: pricingMultiplier(2.5)}, want: 2.5},
- {name: "priority alias", serviceTier: "priority", pricing: &ModelPricing{FastMultiplier: pricingMultiplier(2)}, want: 2},
- {name: "flex configured", serviceTier: "flex", pricing: &ModelPricing{FlexMultiplier: pricingMultiplier(0.4)}, want: 0.4},
- {name: "legacy fast default", serviceTier: "fast", pricing: &ModelPricing{}, want: 2},
- {name: "ultrafast default", serviceTier: "ultrafast", pricing: &ModelPricing{}, want: 2},
- {name: "ultrafast ignores fast multiplier", serviceTier: "ultrafast", pricing: &ModelPricing{FastMultiplier: pricingMultiplier(2.5)}, want: 2},
- {name: "legacy flex default", serviceTier: "flex", pricing: &ModelPricing{}, want: 0.5},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- require.InDelta(t, tt.want, configuredServiceTierMultiplier(tt.serviceTier, tt.pricing), 1e-12)
- })
- }
- }
- func TestConfiguredServiceTierMultiplierAppliesToEveryTokenComponent(t *testing.T) {
- pricing := &ModelPricing{
- InputPricePerToken: 5e-6,
- OutputPricePerToken: 30e-6,
- CacheCreationPricePerToken: 6.25e-6,
- CacheReadPricePerToken: 0.5e-6,
- FastMultiplier: pricingMultiplier(2.5),
- FlexMultiplier: pricingMultiplier(0.5),
- }
- tokens := UsageTokens{
- InputTokens: 1_000_000,
- OutputTokens: 1_000_000,
- CacheCreationTokens: 1_000_000,
- CacheReadTokens: 1_000_000,
- }
- service := &BillingService{}
- fast := service.computeTokenBreakdown(pricing, tokens, 1, "fast", false)
- require.InDelta(t, 12.5, fast.InputCost, 1e-12)
- require.InDelta(t, 75, fast.OutputCost, 1e-12)
- require.InDelta(t, 15.625, fast.CacheCreationCost, 1e-12)
- require.InDelta(t, 1.25, fast.CacheReadCost, 1e-12)
- flex := service.computeTokenBreakdown(pricing, tokens, 1, "flex", false)
- require.InDelta(t, 2.5, flex.InputCost, 1e-12)
- require.InDelta(t, 15, flex.OutputCost, 1e-12)
- require.InDelta(t, 3.125, flex.CacheCreationCost, 1e-12)
- require.InDelta(t, 0.25, flex.CacheReadCost, 1e-12)
- }
- func TestChannelOverridePreservesCatalogFastRatioByDefault(t *testing.T) {
- pricing := &ModelPricing{
- InputPricePerToken: 2,
- InputPricePerTokenPriority: 4,
- OutputPricePerToken: 6,
- OutputPricePerTokenPriority: 12,
- }
- applyChannelTokenPriceOverrides(pricing, &ChannelModelPricing{
- InputPrice: pricingMultiplier(3),
- OutputPrice: pricingMultiplier(9),
- })
- require.InDelta(t, 3, pricing.InputPricePerToken, 1e-12)
- require.InDelta(t, 6, pricing.InputPricePerTokenPriority, 1e-12)
- require.InDelta(t, 9, pricing.OutputPricePerToken, 1e-12)
- require.InDelta(t, 18, pricing.OutputPricePerTokenPriority, 1e-12)
- }
- func TestAnthropicFastUsesDefaultMultiplierWithoutCatalogTier(t *testing.T) {
- pricing := &ModelPricing{InputPricePerToken: 5e-6, OutputPricePerToken: 25e-6}
- cost := (&BillingService{}).computeTokenBreakdown(pricing, UsageTokens{
- InputTokens: 1_000_000, OutputTokens: 1_000_000,
- }, 1, "fast", false)
- require.InDelta(t, 10, cost.InputCost, 1e-12)
- require.InDelta(t, 50, cost.OutputCost, 1e-12)
- }
- func TestBuiltInModelFastDefaults(t *testing.T) {
- service := &BillingService{fallbackPrices: make(map[string]*ModelPricing)}
- service.initFallbackPricing()
- for _, tt := range []struct {
- model string
- want float64
- }{
- {model: "gpt-5.5", want: 2.5},
- {model: "claude-opus-4.8", want: 2},
- {model: "claude-opus-5", want: 2},
- } {
- pricing := service.fallbackPrices[tt.model]
- require.NotNil(t, pricing)
- require.InDelta(t, tt.want, pricing.InputPricePerTokenPriority/pricing.InputPricePerToken, 1e-12)
- require.InDelta(t, tt.want, pricing.OutputPricePerTokenPriority/pricing.OutputPricePerToken, 1e-12)
- }
- }
- func TestIntervalMultipliersApplyToChannelBase(t *testing.T) {
- base := &ModelPricing{
- InputPricePerToken: 5,
- OutputPricePerToken: 30,
- CacheCreationPricePerToken: 6.25,
- CacheCreation5mPrice: 6.25,
- CacheCreation1hPrice: 6.25,
- CacheReadPricePerToken: 0.5,
- FastMultiplier: pricingMultiplier(2),
- FlexMultiplier: pricingMultiplier(0.5),
- }
- resolved := &ResolvedPricing{
- BasePricing: base,
- Intervals: []PricingInterval{{
- MinTokens: 272000,
- InputMultiplier: pricingMultiplier(2),
- OutputMultiplier: pricingMultiplier(1.5),
- CacheWriteMultiplier: pricingMultiplier(2),
- CacheReadMultiplier: pricingMultiplier(2),
- }},
- }
- pricing := (&ModelPricingResolver{}).GetIntervalPricing(resolved, 272001)
- require.InDelta(t, 10, pricing.InputPricePerToken, 1e-12)
- require.InDelta(t, 45, pricing.OutputPricePerToken, 1e-12)
- require.InDelta(t, 12.5, pricing.CacheCreationPricePerToken, 1e-12)
- require.InDelta(t, 1, pricing.CacheReadPricePerToken, 1e-12)
- require.Same(t, base, (&ModelPricingResolver{}).GetIntervalPricing(resolved, 272000))
- }
- func TestIntervalExplicitPriceTakesPrecedenceOverMultiplier(t *testing.T) {
- pricing := intervalToModelPricing(&PricingInterval{
- InputPrice: pricingMultiplier(7),
- InputMultiplier: pricingMultiplier(2),
- }, &ModelPricing{InputPricePerToken: 5}, nil)
- require.InDelta(t, 7, pricing.InputPricePerToken, 1e-12)
- }
- func TestIntervalPricePreservesDefaultFastRatio(t *testing.T) {
- pricing := intervalToModelPricing(&PricingInterval{
- InputPrice: pricingMultiplier(7),
- }, &ModelPricing{
- InputPricePerToken: 5,
- InputPricePerTokenPriority: 10,
- }, nil)
- require.InDelta(t, 7, pricing.InputPricePerToken, 1e-12)
- require.InDelta(t, 14, pricing.InputPricePerTokenPriority, 1e-12)
- }
- func TestAnthropicSpeedServiceTier(t *testing.T) {
- account := &Account{Platform: PlatformAnthropic}
- for _, model := range []string{"claude-opus-5", "claude-opus-4-8", "claude-opus-4.8"} {
- tier := anthropicSpeedServiceTier(account, "fast", model)
- require.NotNil(t, tier, "model %s should bill as fast", model)
- require.Equal(t, "fast", *tier)
- }
- require.Nil(t, anthropicSpeedServiceTier(&Account{Platform: PlatformOpenAI}, "fast", "claude-opus-5"))
- require.Nil(t, anthropicSpeedServiceTier(account, "standard", "claude-opus-5"))
- }
- // fast mode 不存在于这些模型/承载上,即便客户端传了 speed=fast 也不能计 2x。
- func TestAnthropicSpeedServiceTierRejectsUnsupportedTargets(t *testing.T) {
- account := &Account{Platform: PlatformAnthropic}
- for _, model := range []string{
- "claude-opus-4-7", // fast mode 已被移除
- "claude-opus-4-6", //
- "claude-opus-4-5", // 不能被 "opus-5" 规则误判
- "claude-sonnet-5", // 非 Opus
- "claude-haiku-4-5", //
- "", //
- } {
- require.Nil(t, anthropicSpeedServiceTier(account, "fast", model),
- "model %q must not bill as fast", model)
- }
- bedrock := &Account{Platform: PlatformAnthropic, Type: AccountTypeBedrock}
- require.Nil(t, anthropicSpeedServiceTier(bedrock, "fast", "claude-opus-5"))
- }
- func TestAnthropicSpeedModelPrefersMappedUpstreamModel(t *testing.T) {
- parsed := &ParsedRequest{Model: "claude-opus-5"}
- require.Equal(t, "claude-opus-4-7", anthropicSpeedModel(parsed, &ForwardResult{
- UpstreamModel: "claude-opus-4-7",
- }))
- require.Equal(t, "claude-opus-5", anthropicSpeedModel(parsed, &ForwardResult{}))
- }
- func TestMultiplierOnlyIntervalIsValid(t *testing.T) {
- require.NoError(t, ValidateIntervals([]PricingInterval{{
- MinTokens: 199999,
- InputMultiplier: pricingMultiplier(2),
- }}, BillingModeToken))
- require.NoError(t, checkIntervalsHavePrices(ChannelModelPricing{
- Models: []string{"grok-4.6"},
- Intervals: []PricingInterval{{
- MinTokens: 199999,
- InputMultiplier: pricingMultiplier(2),
- }},
- }))
- }
- func TestChannelMultipliersMustBePositive(t *testing.T) {
- zero := 0.0
- require.Error(t, checkPricesNotNegative(ChannelModelPricing{FastMultiplier: &zero}))
- require.Error(t, checkPricesNotNegative(ChannelModelPricing{FlexMultiplier: &zero}))
- require.Error(t, ValidateIntervals([]PricingInterval{{
- MinTokens: 100,
- InputMultiplier: &zero,
- }}, BillingModeToken))
- }
- func TestCalculateTokenCostContextTierEnablement(t *testing.T) {
- base := &ModelPricing{InputPricePerToken: 1e-6}
- resolved := &ResolvedPricing{
- BasePricing: base,
- Intervals: []PricingInterval{{
- MinTokens: 100,
- InputMultiplier: pricingMultiplier(2),
- }},
- }
- resolver := &ModelPricingResolver{}
- service := &BillingService{}
- tokens := UsageTokens{InputTokens: 200}
- t.Run("group disabled uses base tier", func(t *testing.T) {
- resolved.longContextPricingEnabled = false
- cost, err := service.calculateTokenCost(resolved, CostInput{
- Model: "custom", Tokens: tokens, RateMultiplier: 1, Resolver: resolver,
- })
- require.NoError(t, err)
- require.InDelta(t, 200e-6, cost.TotalCost, 1e-12)
- })
- t.Run("group enabled uses interval", func(t *testing.T) {
- resolved.longContextPricingEnabled = true
- accountDisabled := false
- cost, err := service.calculateTokenCost(resolved, CostInput{
- Model: "custom", Tokens: tokens, RateMultiplier: 1, Resolver: resolver,
- LongContextBillingEnabled: &accountDisabled,
- })
- require.NoError(t, err)
- require.InDelta(t, 400e-6, cost.TotalCost, 1e-12)
- })
- t.Run("account enabled overrides disabled group", func(t *testing.T) {
- resolved.longContextPricingEnabled = false
- accountEnabled := true
- cost, err := service.calculateTokenCost(resolved, CostInput{
- Model: "custom", Tokens: tokens, RateMultiplier: 1, Resolver: resolver,
- LongContextBillingEnabled: &accountEnabled,
- })
- require.NoError(t, err)
- require.InDelta(t, 400e-6, cost.TotalCost, 1e-12)
- })
- }
- func TestCalculateTokenCostCombinesIntervalAndFastMultiplier(t *testing.T) {
- resolved := &ResolvedPricing{
- BasePricing: &ModelPricing{
- InputPricePerToken: 1e-6,
- FastMultiplier: pricingMultiplier(2.5),
- },
- Intervals: []PricingInterval{{
- MinTokens: 100,
- InputMultiplier: pricingMultiplier(2),
- }},
- longContextPricingEnabled: true,
- }
- cost, err := (&BillingService{}).calculateTokenCost(resolved, CostInput{
- Model: "custom", Tokens: UsageTokens{InputTokens: 200}, RateMultiplier: 1,
- ServiceTier: "fast", Resolver: &ModelPricingResolver{},
- })
- require.NoError(t, err)
- require.InDelta(t, 1e-3, cost.TotalCost, 1e-12)
- }
|