From c90089c8143d9cee3c0291316d89dde0590ea75c Mon Sep 17 00:00:00 2001 From: alfadb Date: Fri, 12 Jun 2026 10:29:24 +0800 Subject: [PATCH] fix(billing): address Copilot review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 应 PR #3243 review 修正三项问题: 1. 删除 DeepSeek V4 fallback 定价的重复块(cherry-pick 时 的旧位置 + 新国产 LLM 分段都写了一遍,后写覆盖前写, 当前值相同所以无 bug,但冗余,未来改一处易漏另一处)。 2. 删除 minimax-m3 匹配中的死代码——modelLower 已经 strings.ToLower(),'MiniMax-M3' 字面量永远不可能匹中。 3. 测试结构改 *float64 替代 0 sentinel,让 free-tier 模型(GLM-4.5-Flash / GLM-4.7-Flash)的 0 价能被 真正断言而非默默跳过;定义内联 floatPtr 辅助函数。 --- backend/internal/service/billing_service.go | 20 +-- .../internal/service/billing_service_test.go | 147 +++++++++--------- 2 files changed, 75 insertions(+), 92 deletions(-) diff --git a/backend/internal/service/billing_service.go b/backend/internal/service/billing_service.go index a1008e0515..43e91ce3e2 100644 --- a/backend/internal/service/billing_service.go +++ b/backend/internal/service/billing_service.go @@ -294,24 +294,6 @@ func (s *BillingService) initFallbackPricing() { CacheReadPricePerTokenPriority: 0.35e-6, SupportsCacheBreakdown: false, } - // DeepSeek V4 Pro - // Source: https://api-docs.deepseek.com/quick_start/pricing - s.fallbackPrices["deepseek-v4-pro"] = &ModelPricing{ - InputPricePerToken: 4.35e-7, // $0.435 per MTok - OutputPricePerToken: 8.7e-7, // $0.87 per MTok - CacheReadPricePerToken: 3.625e-9, // $0.003625 per MTok - SupportsCacheBreakdown: false, - } - - // DeepSeek V4 Flash - // Source: https://api-docs.deepseek.com/quick_start/pricing - s.fallbackPrices["deepseek-v4-flash"] = &ModelPricing{ - InputPricePerToken: 1.4e-7, // $0.14 per MTok - OutputPricePerToken: 2.8e-7, // $0.28 per MTok - CacheReadPricePerToken: 2.8e-9, // $0.0028 per MTok - SupportsCacheBreakdown: false, - } - // Codex 族兜底统一按 GPT-5.3 Codex 价格计费 s.fallbackPrices["gpt-5.3-codex"] = &ModelPricing{ InputPricePerToken: 1.5e-6, // $1.5 per MTok @@ -610,7 +592,7 @@ func (s *BillingService) getFallbackPricing(model string) *ModelPricing { } // MiniMax M 系列(M3 / M2.7 / M2.5 / M2.1 / M2;含 highspeed 变体) - if strings.Contains(modelLower, "minimax-m3") || strings.Contains(modelLower, "MiniMax-M3") { + if strings.Contains(modelLower, "minimax-m3") { return s.fallbackPrices["minimax-m3"] } if strings.Contains(modelLower, "minimax-m2.7-highspeed") || strings.Contains(modelLower, "minimax-m2-7-highspeed") { diff --git a/backend/internal/service/billing_service_test.go b/backend/internal/service/billing_service_test.go index 4935ad8487..8bd6e41ede 100644 --- a/backend/internal/service/billing_service_test.go +++ b/backend/internal/service/billing_service_test.go @@ -332,13 +332,15 @@ func TestCalculateCost_LongContextAppliesMultiplierToCacheCreation5mAnd1h(t *tes func TestGetFallbackPricing_FamilyMatching(t *testing.T) { svc := newTestBillingService() - // expectedOutput / expectedCacheRead 为 0 时跳过该字段断言(保持与原有用例兼容)。 + floatPtr := func(v float64) *float64 { return &v } + + // expectedOutput / expectedCacheRead 为 nil 时跳过该字段断言(保持与原有用例兼容)。 tests := []struct { name string model string expectedInput float64 - expectedOutput float64 - expectedCacheRead float64 + expectedOutput *float64 + expectedCacheRead *float64 expectNilPricing bool }{ {name: "empty model", model: " ", expectNilPricing: true}, @@ -359,29 +361,29 @@ func TestGetFallbackPricing_FamilyMatching(t *testing.T) { name: "deepseek v4 pro", model: "deepseek-v4-pro", expectedInput: 4.35e-7, - expectedOutput: 8.7e-7, - expectedCacheRead: 3.625e-9, + expectedOutput: floatPtr(8.7e-7), + expectedCacheRead: floatPtr(3.625e-9), }, { name: "deepseek v4 flash", model: "deepseek-v4-flash", expectedInput: 1.4e-7, - expectedOutput: 2.8e-7, - expectedCacheRead: 2.8e-9, + expectedOutput: floatPtr(2.8e-7), + expectedCacheRead: floatPtr(2.8e-9), }, { name: "deepseek chat alias → flash", model: "deepseek-chat", expectedInput: 1.4e-7, - expectedOutput: 2.8e-7, - expectedCacheRead: 2.8e-9, + expectedOutput: floatPtr(2.8e-7), + expectedCacheRead: floatPtr(2.8e-9), }, { name: "deepseek reasoner alias → flash", model: "deepseek-reasoner", expectedInput: 1.4e-7, - expectedOutput: 2.8e-7, - expectedCacheRead: 2.8e-9, + expectedOutput: floatPtr(2.8e-7), + expectedCacheRead: floatPtr(2.8e-9), }, // ---- 智谱 GLM(z.ai USD 口径)---- @@ -389,100 +391,99 @@ func TestGetFallbackPricing_FamilyMatching(t *testing.T) { name: "glm 5.1 flagship", model: "glm-5.1", expectedInput: 1.4e-6, - expectedOutput: 4.4e-6, - expectedCacheRead: 0.26e-6, + expectedOutput: floatPtr(4.4e-6), + expectedCacheRead: floatPtr(0.26e-6), }, { name: "glm 5 base", model: "glm-5", expectedInput: 1e-6, - expectedOutput: 3.2e-6, - expectedCacheRead: 0.2e-6, + expectedOutput: floatPtr(3.2e-6), + expectedCacheRead: floatPtr(0.2e-6), }, { name: "glm 5 turbo", model: "glm-5-turbo", expectedInput: 1.2e-6, - expectedOutput: 4e-6, - expectedCacheRead: 0.24e-6, + expectedOutput: floatPtr(4e-6), + expectedCacheRead: floatPtr(0.24e-6), }, { name: "glm 4.7", model: "glm-4.7", expectedInput: 0.6e-6, - expectedOutput: 2.2e-6, - expectedCacheRead: 0.11e-6, + expectedOutput: floatPtr(2.2e-6), + expectedCacheRead: floatPtr(0.11e-6), }, { name: "glm 4.6", model: "glm-4.6", expectedInput: 0.6e-6, - expectedOutput: 2.2e-6, - expectedCacheRead: 0.11e-6, + expectedOutput: floatPtr(2.2e-6), + expectedCacheRead: floatPtr(0.11e-6), }, { name: "glm 4.5", model: "glm-4.5", expectedInput: 0.6e-6, - expectedOutput: 2.2e-6, - expectedCacheRead: 0.11e-6, + expectedOutput: floatPtr(2.2e-6), + expectedCacheRead: floatPtr(0.11e-6), }, { name: "glm 4.5-x premium", model: "glm-4.5-x", expectedInput: 2.2e-6, - expectedOutput: 8.9e-6, - expectedCacheRead: 0.45e-6, + expectedOutput: floatPtr(8.9e-6), + expectedCacheRead: floatPtr(0.45e-6), }, { name: "glm 4.5-air lightweight", model: "glm-4.5-air", expectedInput: 0.2e-6, - expectedOutput: 1.1e-6, - expectedCacheRead: 0.03e-6, + expectedOutput: floatPtr(1.1e-6), + expectedCacheRead: floatPtr(0.03e-6), }, { name: "glm 4.7-flashx", model: "glm-4.7-flashx", expectedInput: 0.07e-6, - expectedOutput: 0.4e-6, - expectedCacheRead: 0.01e-6, + expectedOutput: floatPtr(0.4e-6), + expectedCacheRead: floatPtr(0.01e-6), }, { name: "glm 4.5-flash free tier", model: "glm-4.5-flash", expectedInput: 0, // Free tier on z.ai - expectedOutput: 0, - expectedCacheRead: 0, + expectedOutput: floatPtr(0), + expectedCacheRead: floatPtr(0), }, { name: "glm 4.7-flash free tier", model: "glm-4.7-flash", expectedInput: 0, - expectedOutput: 0, - expectedCacheRead: 0, + expectedOutput: floatPtr(0), + expectedCacheRead: floatPtr(0), }, { - name: "glm 4-32b legacy", - model: "glm-4-32b-0414-128k", - expectedInput: 0.1e-6, - expectedOutput: 0.1e-6, - expectedCacheRead: 0, + name: "glm 4-32b legacy", + model: "glm-4-32b-0414-128k", + expectedInput: 0.1e-6, + expectedOutput: floatPtr(0.1e-6), }, // 关键:5.1 必须先于 5 匹配(避免被 glm-5 抢走) { name: "glm 5.1 vs glm 5 ordering (verbatim 5.1)", model: "glm-5.1", expectedInput: 1.4e-6, // = glm-5.1 价格 - expectedOutput: 4.4e-6, - expectedCacheRead: 0.26e-6, + expectedOutput: floatPtr(4.4e-6), + expectedCacheRead: floatPtr(0.26e-6), }, { name: "glm 4.5-air vs glm 4.5 ordering", model: "glm-4.5-air", expectedInput: 0.2e-6, // = glm-4.5-air 价格(不是 glm-4.5 的 0.6e-6) - expectedOutput: 1.1e-6, - expectedCacheRead: 0.03e-6, + expectedOutput: floatPtr(1.1e-6), + expectedCacheRead: floatPtr(0.03e-6), }, // ---- 月之暗面 Kimi ---- @@ -490,44 +491,44 @@ func TestGetFallbackPricing_FamilyMatching(t *testing.T) { name: "kimi k2.6 flagship", model: "kimi-k2.6", expectedInput: 0.95e-6, - expectedOutput: 4e-6, - expectedCacheRead: 0.15e-6, + expectedOutput: floatPtr(4e-6), + expectedCacheRead: floatPtr(0.15e-6), }, { name: "kimi k2.5", model: "kimi-k2.5", expectedInput: 0.60e-6, - expectedOutput: 3e-6, - expectedCacheRead: 0.098e-6, + expectedOutput: floatPtr(3e-6), + expectedCacheRead: floatPtr(0.098e-6), }, { name: "kimi k2-thinking", model: "kimi-k2-thinking", expectedInput: 0.56e-6, - expectedOutput: 2.24e-6, - expectedCacheRead: 0.14e-6, + expectedOutput: floatPtr(2.24e-6), + expectedCacheRead: floatPtr(0.14e-6), }, { name: "kimi k2 base", model: "kimi-k2", expectedInput: 0.56e-6, - expectedOutput: 2.24e-6, - expectedCacheRead: 0.14e-6, + expectedOutput: floatPtr(2.24e-6), + expectedCacheRead: floatPtr(0.14e-6), }, // 关键:k2.6 / k2.5 / k2-thinking 必须先于 k2 匹配 { name: "kimi k2.6 vs k2 ordering", model: "kimi-k2.6", expectedInput: 0.95e-6, // = k2.6 不是 k2 的 0.56e-6 - expectedOutput: 4e-6, - expectedCacheRead: 0.15e-6, + expectedOutput: floatPtr(4e-6), + expectedCacheRead: floatPtr(0.15e-6), }, { name: "kimi k2 thinking hyphenated variant", model: "kimi-k2-thinking-preview", expectedInput: 0.56e-6, - expectedOutput: 2.24e-6, - expectedCacheRead: 0.14e-6, + expectedOutput: floatPtr(2.24e-6), + expectedCacheRead: floatPtr(0.14e-6), }, // ---- MiniMax M 系列 ---- @@ -535,46 +536,46 @@ func TestGetFallbackPricing_FamilyMatching(t *testing.T) { name: "minimax m3", model: "minimax-m3", expectedInput: 0.60e-6, - expectedOutput: 2.40e-6, - expectedCacheRead: 0.12e-6, + expectedOutput: floatPtr(2.40e-6), + expectedCacheRead: floatPtr(0.12e-6), }, { name: "minimax m3 long ctx boundary keep standard tier", model: "minimax-m3-long", // 仍按 standard tier (≤512K) expectedInput: 0.60e-6, - expectedOutput: 2.40e-6, - expectedCacheRead: 0.12e-6, + expectedOutput: floatPtr(2.40e-6), + expectedCacheRead: floatPtr(0.12e-6), }, { name: "minimax m2.7", model: "minimax-m2.7", expectedInput: 0.30e-6, - expectedOutput: 1.20e-6, - expectedCacheRead: 0.06e-6, + expectedOutput: floatPtr(1.20e-6), + expectedCacheRead: floatPtr(0.06e-6), }, { name: "minimax m2.7 highspeed", model: "minimax-m2.7-highspeed", expectedInput: 0.60e-6, - expectedOutput: 2.40e-6, - expectedCacheRead: 0.06e-6, + expectedOutput: floatPtr(2.40e-6), + expectedCacheRead: floatPtr(0.06e-6), }, { name: "minimax m2.5", model: "minimax-m2.5", expectedInput: 0.30e-6, - expectedOutput: 1.20e-6, - expectedCacheRead: 0.03e-6, + expectedOutput: floatPtr(1.20e-6), + expectedCacheRead: floatPtr(0.03e-6), }, { name: "minimax m2 legacy", model: "minimax-m2", expectedInput: 0.30e-6, - expectedOutput: 1.20e-6, - expectedCacheRead: 0.03e-6, + expectedOutput: floatPtr(1.20e-6), + expectedCacheRead: floatPtr(0.03e-6), }, - // ---- 负向用例:未覆盖的国产厂商 / alias 不应误计价 ---- + // ---- 负向用例 ---- {name: "qwen unknown no fallback", model: "qwen-max", expectNilPricing: true}, {name: "doubao unknown no fallback", model: "doubao-pro", expectNilPricing: true}, {name: "hunyuan unknown no fallback", model: "hunyuan-t1", expectNilPricing: true}, @@ -585,8 +586,8 @@ func TestGetFallbackPricing_FamilyMatching(t *testing.T) { name: "kimi k2-0905-preview implicit fallback to k2", model: "kimi-k2-0905-preview", expectedInput: 0.56e-6, - expectedOutput: 2.24e-6, - expectedCacheRead: 0.14e-6, + expectedOutput: floatPtr(2.24e-6), + expectedCacheRead: floatPtr(0.14e-6), }, } @@ -599,12 +600,12 @@ func TestGetFallbackPricing_FamilyMatching(t *testing.T) { } require.NotNil(t, pricing) require.InDelta(t, tt.expectedInput, pricing.InputPricePerToken, 1e-12) - if tt.expectedOutput != 0 { - require.InDelta(t, tt.expectedOutput, pricing.OutputPricePerToken, 1e-12, + if tt.expectedOutput != nil { + require.InDelta(t, *tt.expectedOutput, pricing.OutputPricePerToken, 1e-12, "OutputPricePerToken mismatch for %s", tt.model) } - if tt.expectedCacheRead != 0 { - require.InDelta(t, tt.expectedCacheRead, pricing.CacheReadPricePerToken, 1e-14, + if tt.expectedCacheRead != nil { + require.InDelta(t, *tt.expectedCacheRead, pricing.CacheReadPricePerToken, 1e-14, "CacheReadPricePerToken mismatch for %s", tt.model) } })