Skip to content

Commit 815627d

Browse files
committed
修复 Fast 模式渠道定价倍率计算
渠道自定义普通单价不再作为 priority 专用单价使用。Fast 模式现在按计费项分别应用 priority 价格或 2 倍倍率,并补充 gpt-5.5 渠道定价 Fast 计费回归测试。验证已通过:go test -tags unit ./internal/service 计费相关用例。本次提交文件敏感形态扫描为空;仓库既有 P0 命中已由用户确认为公开可接受内容。
1 parent e6ebea3 commit 815627d

4 files changed

Lines changed: 118 additions & 36 deletions

File tree

backend/internal/service/billing_service.go

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -363,11 +363,11 @@ func (s *BillingService) GetModelPricingWithChannel(model string, channelPricing
363363
}
364364
if channelPricing.InputPrice != nil {
365365
pricing.InputPricePerToken = *channelPricing.InputPrice
366-
pricing.InputPricePerTokenPriority = *channelPricing.InputPrice
366+
pricing.InputPricePerTokenPriority = 0
367367
}
368368
if channelPricing.OutputPrice != nil {
369369
pricing.OutputPricePerToken = *channelPricing.OutputPrice
370-
pricing.OutputPricePerTokenPriority = *channelPricing.OutputPrice
370+
pricing.OutputPricePerTokenPriority = 0
371371
}
372372
if channelPricing.CacheWritePrice != nil {
373373
pricing.CacheCreationPricePerToken = *channelPricing.CacheWritePrice
@@ -376,7 +376,7 @@ func (s *BillingService) GetModelPricingWithChannel(model string, channelPricing
376376
}
377377
if channelPricing.CacheReadPrice != nil {
378378
pricing.CacheReadPricePerToken = *channelPricing.CacheReadPrice
379-
pricing.CacheReadPricePerTokenPriority = *channelPricing.CacheReadPrice
379+
pricing.CacheReadPricePerTokenPriority = 0
380380
}
381381
if channelPricing.ImageOutputPrice != nil {
382382
pricing.ImageOutputPricePerToken = *channelPricing.ImageOutputPrice
@@ -471,20 +471,38 @@ func (s *BillingService) computeTokenBreakdown(
471471
inputPrice := pricing.InputPricePerToken
472472
outputPrice := pricing.OutputPricePerToken
473473
cacheReadPrice := pricing.CacheReadPricePerToken
474-
tierMultiplier := 1.0
474+
inputTierMultiplier := 1.0
475+
outputTierMultiplier := 1.0
476+
cacheCreationTierMultiplier := 1.0
477+
cacheReadTierMultiplier := 1.0
478+
imageOutputTierMultiplier := 1.0
475479

476-
if usePriorityServiceTierPricing(serviceTier, pricing) {
480+
switch normalizeBillingServiceTier(serviceTier) {
481+
case "priority":
477482
if pricing.InputPricePerTokenPriority > 0 {
478483
inputPrice = pricing.InputPricePerTokenPriority
484+
} else {
485+
inputTierMultiplier = serviceTierCostMultiplier(serviceTier)
479486
}
480487
if pricing.OutputPricePerTokenPriority > 0 {
481488
outputPrice = pricing.OutputPricePerTokenPriority
489+
} else {
490+
outputTierMultiplier = serviceTierCostMultiplier(serviceTier)
482491
}
483492
if pricing.CacheReadPricePerTokenPriority > 0 {
484493
cacheReadPrice = pricing.CacheReadPricePerTokenPriority
494+
} else {
495+
cacheReadTierMultiplier = serviceTierCostMultiplier(serviceTier)
485496
}
486-
} else {
487-
tierMultiplier = serviceTierCostMultiplier(serviceTier)
497+
cacheCreationTierMultiplier = serviceTierCostMultiplier(serviceTier)
498+
imageOutputTierMultiplier = serviceTierCostMultiplier(serviceTier)
499+
case "flex":
500+
tierMultiplier := serviceTierCostMultiplier(serviceTier)
501+
inputTierMultiplier = tierMultiplier
502+
outputTierMultiplier = tierMultiplier
503+
cacheCreationTierMultiplier = tierMultiplier
504+
cacheReadTierMultiplier = tierMultiplier
505+
imageOutputTierMultiplier = tierMultiplier
488506
}
489507

490508
if applyLongCtx && s.shouldApplySessionLongContextPricing(tokens, pricing) {
@@ -493,36 +511,28 @@ func (s *BillingService) computeTokenBreakdown(
493511
}
494512

495513
bd := &CostBreakdown{}
496-
bd.InputCost = float64(tokens.InputTokens) * inputPrice
514+
bd.InputCost = float64(tokens.InputTokens) * inputPrice * inputTierMultiplier
497515

498516
// 分离图片输出 token 与文本输出 token
499517
textOutputTokens := tokens.OutputTokens - tokens.ImageOutputTokens
500518
if textOutputTokens < 0 {
501519
textOutputTokens = 0
502520
}
503-
bd.OutputCost = float64(textOutputTokens) * outputPrice
521+
bd.OutputCost = float64(textOutputTokens) * outputPrice * outputTierMultiplier
504522

505523
// 图片输出 token 费用(独立费率)
506524
if tokens.ImageOutputTokens > 0 {
507525
imgPrice := pricing.ImageOutputPricePerToken
508526
if imgPrice == 0 {
509527
imgPrice = outputPrice // 回退到常规输出价格
510528
}
511-
bd.ImageOutputCost = float64(tokens.ImageOutputTokens) * imgPrice
529+
bd.ImageOutputCost = float64(tokens.ImageOutputTokens) * imgPrice * imageOutputTierMultiplier
512530
}
513531

514532
// 缓存创建费用
515-
bd.CacheCreationCost = s.computeCacheCreationCost(pricing, tokens)
516-
517-
bd.CacheReadCost = float64(tokens.CacheReadTokens) * cacheReadPrice
533+
bd.CacheCreationCost = s.computeCacheCreationCost(pricing, tokens) * cacheCreationTierMultiplier
518534

519-
if tierMultiplier != 1.0 {
520-
bd.InputCost *= tierMultiplier
521-
bd.OutputCost *= tierMultiplier
522-
bd.ImageOutputCost *= tierMultiplier
523-
bd.CacheCreationCost *= tierMultiplier
524-
bd.CacheReadCost *= tierMultiplier
525-
}
535+
bd.CacheReadCost = float64(tokens.CacheReadTokens) * cacheReadPrice * cacheReadTierMultiplier
526536

527537
bd.TotalCost = bd.InputCost + bd.OutputCost + bd.ImageOutputCost +
528538
bd.CacheCreationCost + bd.CacheReadCost

backend/internal/service/billing_service_test.go

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,37 @@ func TestCalculateCostWithServiceTier_OpenAIPriorityUsesPriorityPricing(t *testi
474474
require.InDelta(t, baseCost.TotalCost*2, priorityCost.TotalCost, 1e-10)
475475
}
476476

477+
func TestCalculateCostWithServiceTier_PriorityAppliesPerComponentFallback(t *testing.T) {
478+
svc := newTestBillingService()
479+
pricing := &ModelPricing{
480+
InputPricePerToken: 5e-6,
481+
OutputPricePerToken: 15e-6,
482+
OutputPricePerTokenPriority: 30e-6,
483+
CacheReadPricePerToken: 0.25e-6,
484+
CacheReadPricePerTokenPriority: 0.5e-6,
485+
CacheCreationPricePerToken: 2.5e-6,
486+
}
487+
488+
cost := svc.computeTokenBreakdown(
489+
pricing,
490+
UsageTokens{
491+
InputTokens: 100_000,
492+
OutputTokens: 100_000,
493+
CacheCreationTokens: 100_000,
494+
CacheReadTokens: 100_000,
495+
},
496+
1.0,
497+
"priority",
498+
false,
499+
)
500+
501+
require.InDelta(t, 1, cost.InputCost, 1e-12)
502+
require.InDelta(t, 3, cost.OutputCost, 1e-12)
503+
require.InDelta(t, 0.5, cost.CacheCreationCost, 1e-12)
504+
require.InDelta(t, 0.05, cost.CacheReadCost, 1e-12)
505+
require.InDelta(t, 4.55, cost.TotalCost, 1e-12)
506+
}
507+
477508
func TestCalculateCostWithServiceTier_FlexAppliesHalfMultiplier(t *testing.T) {
478509
svc := newTestBillingService()
479510
tokens := UsageTokens{InputTokens: 100, OutputTokens: 50, CacheCreationTokens: 40, CacheReadTokens: 20}
@@ -699,9 +730,9 @@ func TestGetModelPricingWithChannel_OverrideInputPriceOnly(t *testing.T) {
699730
pricing, err := svc.GetModelPricingWithChannel("claude-sonnet-4", chPricing)
700731
require.NoError(t, err)
701732

702-
// InputPrice overridden (both normal and priority)
733+
// InputPrice overridden; priority remains empty so service_tier multiplier can apply.
703734
require.InDelta(t, 99e-6, pricing.InputPricePerToken, 1e-12)
704-
require.InDelta(t, 99e-6, pricing.InputPricePerTokenPriority, 1e-12)
735+
require.Zero(t, pricing.InputPricePerTokenPriority)
705736

706737
// OutputPrice unchanged (claude-sonnet-4 fallback = 15e-6)
707738
require.InDelta(t, 15e-6, pricing.OutputPricePerToken, 1e-12)
@@ -718,7 +749,7 @@ func TestGetModelPricingWithChannel_OverrideOutputPriceOnly(t *testing.T) {
718749

719750
// OutputPrice overridden
720751
require.InDelta(t, 88e-6, pricing.OutputPricePerToken, 1e-12)
721-
require.InDelta(t, 88e-6, pricing.OutputPricePerTokenPriority, 1e-12)
752+
require.Zero(t, pricing.OutputPricePerTokenPriority)
722753

723754
// InputPrice unchanged (claude-sonnet-4 fallback = 3e-6)
724755
require.InDelta(t, 3e-6, pricing.InputPricePerToken, 1e-12)
@@ -738,14 +769,14 @@ func TestGetModelPricingWithChannel_OverrideAllFields(t *testing.T) {
738769
require.NoError(t, err)
739770

740771
require.InDelta(t, 10e-6, pricing.InputPricePerToken, 1e-12)
741-
require.InDelta(t, 10e-6, pricing.InputPricePerTokenPriority, 1e-12)
772+
require.Zero(t, pricing.InputPricePerTokenPriority)
742773
require.InDelta(t, 20e-6, pricing.OutputPricePerToken, 1e-12)
743-
require.InDelta(t, 20e-6, pricing.OutputPricePerTokenPriority, 1e-12)
774+
require.Zero(t, pricing.OutputPricePerTokenPriority)
744775
require.InDelta(t, 5e-6, pricing.CacheCreationPricePerToken, 1e-12)
745776
require.InDelta(t, 5e-6, pricing.CacheCreation5mPrice, 1e-12)
746777
require.InDelta(t, 5e-6, pricing.CacheCreation1hPrice, 1e-12)
747778
require.InDelta(t, 1e-6, pricing.CacheReadPricePerToken, 1e-12)
748-
require.InDelta(t, 1e-6, pricing.CacheReadPricePerTokenPriority, 1e-12)
779+
require.Zero(t, pricing.CacheReadPricePerTokenPriority)
749780
require.InDelta(t, 50e-6, pricing.ImageOutputPricePerToken, 1e-12)
750781
}
751782

@@ -773,9 +804,9 @@ func TestGetModelPricingWithChannel_CacheReadPriceAffectsPriority(t *testing.T)
773804
pricing, err := svc.GetModelPricingWithChannel("claude-sonnet-4", chPricing)
774805
require.NoError(t, err)
775806

776-
// CacheReadPrice should set both normal and priority
807+
// CacheReadPrice should set only normal; priority multiplier is applied later.
777808
require.InDelta(t, 2e-6, pricing.CacheReadPricePerToken, 1e-12)
778-
require.InDelta(t, 2e-6, pricing.CacheReadPricePerTokenPriority, 1e-12)
809+
require.Zero(t, pricing.CacheReadPricePerTokenPriority)
779810
}
780811

781812
func TestGetModelPricingWithChannel_UnknownModelReturnsError(t *testing.T) {

backend/internal/service/model_pricing_resolver.go

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -151,11 +151,11 @@ func (r *ModelPricingResolver) applyTokenOverrides(chPricing *ChannelModelPricin
151151

152152
if chPricing.InputPrice != nil {
153153
resolved.BasePricing.InputPricePerToken = *chPricing.InputPrice
154-
resolved.BasePricing.InputPricePerTokenPriority = *chPricing.InputPrice
154+
resolved.BasePricing.InputPricePerTokenPriority = 0
155155
}
156156
if chPricing.OutputPrice != nil {
157157
resolved.BasePricing.OutputPricePerToken = *chPricing.OutputPrice
158-
resolved.BasePricing.OutputPricePerTokenPriority = *chPricing.OutputPrice
158+
resolved.BasePricing.OutputPricePerTokenPriority = 0
159159
}
160160
if chPricing.CacheWritePrice != nil {
161161
resolved.BasePricing.CacheCreationPricePerToken = *chPricing.CacheWritePrice
@@ -164,7 +164,7 @@ func (r *ModelPricingResolver) applyTokenOverrides(chPricing *ChannelModelPricin
164164
}
165165
if chPricing.CacheReadPrice != nil {
166166
resolved.BasePricing.CacheReadPricePerToken = *chPricing.CacheReadPrice
167-
resolved.BasePricing.CacheReadPricePerTokenPriority = *chPricing.CacheReadPrice
167+
resolved.BasePricing.CacheReadPricePerTokenPriority = 0
168168
}
169169
if chPricing.ImageOutputPrice != nil {
170170
resolved.BasePricing.ImageOutputPricePerToken = *chPricing.ImageOutputPrice
@@ -215,11 +215,9 @@ func intervalToModelPricing(iv *PricingInterval, supportsCacheBreakdown bool) *M
215215
}
216216
if iv.InputPrice != nil {
217217
pricing.InputPricePerToken = *iv.InputPrice
218-
pricing.InputPricePerTokenPriority = *iv.InputPrice
219218
}
220219
if iv.OutputPrice != nil {
221220
pricing.OutputPricePerToken = *iv.OutputPrice
222-
pricing.OutputPricePerTokenPriority = *iv.OutputPrice
223221
}
224222
if iv.CacheWritePrice != nil {
225223
pricing.CacheCreationPricePerToken = *iv.CacheWritePrice
@@ -228,7 +226,6 @@ func intervalToModelPricing(iv *PricingInterval, supportsCacheBreakdown bool) *M
228226
}
229227
if iv.CacheReadPrice != nil {
230228
pricing.CacheReadPricePerToken = *iv.CacheReadPrice
231-
pricing.CacheReadPricePerTokenPriority = *iv.CacheReadPrice
232229
}
233230
return pricing
234231
}

backend/internal/service/model_pricing_resolver_test.go

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,16 @@ func newTestBillingServiceForResolver() *BillingService {
2121
CacheReadPricePerToken: 0.3e-6,
2222
SupportsCacheBreakdown: false,
2323
}
24+
bs.fallbackPrices["gpt-5.5"] = &ModelPricing{
25+
InputPricePerToken: 2.5e-6,
26+
InputPricePerTokenPriority: 5e-6,
27+
OutputPricePerToken: 15e-6,
28+
OutputPricePerTokenPriority: 30e-6,
29+
CacheCreationPricePerToken: 2.5e-6,
30+
CacheReadPricePerToken: 0.25e-6,
31+
CacheReadPricePerTokenPriority: 0.5e-6,
32+
SupportsCacheBreakdown: false,
33+
}
2434
return bs
2535
}
2636

@@ -215,9 +225,43 @@ func TestResolve_WithChannelOverride_TokenFlat(t *testing.T) {
215225
require.Equal(t, "channel", resolved.Source)
216226
require.NotNil(t, resolved.BasePricing)
217227
require.InDelta(t, 10e-6, resolved.BasePricing.InputPricePerToken, 1e-12)
218-
require.InDelta(t, 10e-6, resolved.BasePricing.InputPricePerTokenPriority, 1e-12)
228+
require.Zero(t, resolved.BasePricing.InputPricePerTokenPriority)
219229
require.InDelta(t, 50e-6, resolved.BasePricing.OutputPricePerToken, 1e-12)
220-
require.InDelta(t, 50e-6, resolved.BasePricing.OutputPricePerTokenPriority, 1e-12)
230+
require.Zero(t, resolved.BasePricing.OutputPricePerTokenPriority)
231+
}
232+
233+
func TestResolve_WithChannelOverride_PriorityUsesTierMultiplierOnChannelPrice(t *testing.T) {
234+
r := newResolverWithChannel(t, []ChannelModelPricing{{
235+
Platform: "anthropic",
236+
Models: []string{"gpt-5.5"},
237+
BillingMode: BillingModeToken,
238+
InputPrice: testPtrFloat64(5e-6),
239+
OutputPrice: testPtrFloat64(30e-6),
240+
CacheReadPrice: testPtrFloat64(0.5e-6),
241+
}})
242+
gid := groupIDPtr()
243+
bs := newTestBillingServiceForResolver()
244+
245+
cost, err := bs.CalculateCostUnified(CostInput{
246+
Ctx: context.Background(),
247+
Model: "gpt-5.5",
248+
GroupID: gid,
249+
Tokens: UsageTokens{
250+
InputTokens: 100_000,
251+
OutputTokens: 100_000,
252+
CacheReadTokens: 10_000,
253+
},
254+
RateMultiplier: 0.06,
255+
ServiceTier: "priority",
256+
Resolver: r,
257+
})
258+
259+
require.NoError(t, err)
260+
require.InDelta(t, 1, cost.InputCost, 1e-12)
261+
require.InDelta(t, 6, cost.OutputCost, 1e-12)
262+
require.InDelta(t, 0.01, cost.CacheReadCost, 1e-12)
263+
require.InDelta(t, 7.01, cost.TotalCost, 1e-12)
264+
require.InDelta(t, 0.4206, cost.ActualCost, 1e-12)
221265
}
222266

223267
func TestResolve_WithChannelOverride_TokenPartialOverride(t *testing.T) {

0 commit comments

Comments
 (0)