Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
289 lines
10 KiB
Go
289 lines
10 KiB
Go
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)
|
|
}
|