Merge pull request #3836 from ShuYeJang/main

fix(billing): 渠道定价覆盖写穿 fallbackPrices 共享指针导致全局计费污染
This commit is contained in:
Wesley Liddick
2026-07-09 14:37:05 +08:00
committed by GitHub
3 changed files with 71 additions and 0 deletions
@@ -771,6 +771,9 @@ func (s *BillingService) GetModelPricingWithChannel(model string, channelPricing
if channelPricing == nil {
return pricing, nil
}
// 防止修改 fallbackPrices 中的共享指针
cloned := *pricing
pricing = &cloned
if channelPricing.InputPrice != nil {
pricing.InputPricePerToken = *channelPricing.InputPrice
pricing.InputPricePerTokenPriority = *channelPricing.InputPrice
@@ -150,6 +150,10 @@ func (r *ModelPricingResolver) applyTokenOverrides(chPricing *ChannelModelPricin
// 区间不匹配时回退到 BasePricing,也需要覆盖图片价格
if resolved.BasePricing == nil {
resolved.BasePricing = &ModelPricing{}
} else {
// 防止修改 fallbackPrices 中的共享指针
cloned := *resolved.BasePricing
resolved.BasePricing = &cloned
}
if chPricing.ImageOutputPrice != nil {
resolved.BasePricing.ImageOutputPricePerToken = *chPricing.ImageOutputPrice
@@ -163,6 +167,10 @@ func (r *ModelPricingResolver) applyTokenOverrides(chPricing *ChannelModelPricin
// 否则用 flat 字段覆盖 BasePricing
if resolved.BasePricing == nil {
resolved.BasePricing = &ModelPricing{}
} else {
// 防止修改 fallbackPrices 中的共享指针
cloned := *resolved.BasePricing
resolved.BasePricing = &cloned
}
if chPricing.InputPrice != nil {
@@ -727,3 +727,63 @@ func TestApplyTokenOverrides_IntervalSetsImageOutputPriceExplicit(t *testing.T)
require.True(t, pricing.ImageOutputPriceExplicit)
require.Equal(t, 0.0, pricing.ImageOutputPricePerToken)
}
// ===========================================================================
// 10. Regression: channel overrides must not pollute fallbackPrices
// ===========================================================================
// TestApplyTokenOverrides_FlatDoesNotPolluteFallbackPrices verifies that the
// flat-override path in applyTokenOverrides clones the BasePricing struct
// before mutation, so the shared fallbackPrices map entry is not written through.
func TestApplyTokenOverrides_FlatDoesNotPolluteFallbackPrices(t *testing.T) {
r := newResolverWithChannel(t, []ChannelModelPricing{{
Platform: "anthropic",
Models: []string{"claude-sonnet-4"},
BillingMode: BillingModeToken,
InputPrice: testPtrFloat64(10e-6), // base is 3e-6
OutputPrice: testPtrFloat64(50e-6), // base is 15e-6
}})
resolved := r.Resolve(context.Background(), PricingInput{
Model: "claude-sonnet-4",
GroupID: groupIDPtr(),
})
// Resolved pricing should reflect the channel override
require.NotNil(t, resolved)
require.InDelta(t, 10e-6, resolved.BasePricing.InputPricePerToken, 1e-12)
require.InDelta(t, 50e-6, resolved.BasePricing.OutputPricePerToken, 1e-12)
// Global fallbackPrices must NOT be polluted
fp := r.billingService.fallbackPrices["claude-sonnet-4"]
require.InDelta(t, 3e-6, fp.InputPricePerToken, 1e-12, "fallback InputPricePerToken polluted")
require.InDelta(t, 15e-6, fp.OutputPricePerToken, 1e-12, "fallback OutputPricePerToken polluted")
require.False(t, fp.ImageOutputPriceExplicit, "fallback ImageOutputPriceExplicit polluted")
}
// TestApplyTokenOverrides_IntervalDoesNotPolluteFallbackPrices verifies that
// the interval-override path also clones before mutation.
func TestApplyTokenOverrides_IntervalDoesNotPolluteFallbackPrices(t *testing.T) {
r := newResolverWithChannel(t, []ChannelModelPricing{{
Platform: "anthropic",
Models: []string{"claude-sonnet-4"},
BillingMode: BillingModeToken,
Intervals: []PricingInterval{
{MinTokens: 0, MaxTokens: testPtrInt(100000), InputPrice: testPtrFloat64(2e-6), OutputPrice: testPtrFloat64(8e-6)},
},
}})
resolved := r.Resolve(context.Background(), PricingInput{
Model: "claude-sonnet-4",
GroupID: groupIDPtr(),
})
require.NotNil(t, resolved)
require.True(t, resolved.BasePricing.ImageOutputPriceExplicit)
// Global fallbackPrices must NOT be polluted
fp := r.billingService.fallbackPrices["claude-sonnet-4"]
require.InDelta(t, 3e-6, fp.InputPricePerToken, 1e-12, "fallback InputPricePerToken polluted")
require.InDelta(t, 15e-6, fp.OutputPricePerToken, 1e-12, "fallback OutputPricePerToken polluted")
require.False(t, fp.ImageOutputPriceExplicit, "fallback ImageOutputPriceExplicit polluted")
}