mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
Merge pull request #3836 from ShuYeJang/main
fix(billing): 渠道定价覆盖写穿 fallbackPrices 共享指针导致全局计费污染
This commit is contained in:
@@ -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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user