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: "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) }