From c906bf000e0c425dcc1fa63f4f7989f92d9ba4d5 Mon Sep 17 00:00:00 2001 From: alfadb Date: Sat, 2 May 2026 11:14:25 +0800 Subject: [PATCH 1/8] feat(billing): add DeepSeek V4 Pro / Flash fallback pricing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pricing source: https://api-docs.deepseek.com/quick_start/pricing deepseek-v4-pro: input $0.435/M, output $0.87/M, cache_read $0.003625/M deepseek-v4-flash: input $0.14/M, output $0.28/M, cache_read $0.0028/M Also map legacy aliases deepseek-chat / deepseek-reasoner → v4-flash. --- backend/internal/service/billing_service.go | 30 +++++++++++++++++++ .../internal/service/billing_service_test.go | 4 +++ 2 files changed, 34 insertions(+) diff --git a/backend/internal/service/billing_service.go b/backend/internal/service/billing_service.go index 75771f23b9..c4e376fcff 100644 --- a/backend/internal/service/billing_service.go +++ b/backend/internal/service/billing_service.go @@ -294,6 +294,24 @@ 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 @@ -344,6 +362,18 @@ func (s *BillingService) getFallbackPricing(model string) *ModelPricing { return s.fallbackPrices["gemini-3.1-pro"] } + // DeepSeek V4 系列 + if strings.Contains(modelLower, "deepseek-v4-flash") { + return s.fallbackPrices["deepseek-v4-flash"] + } + if strings.Contains(modelLower, "deepseek-v4-pro") { + return s.fallbackPrices["deepseek-v4-pro"] + } + // deepseek-chat / deepseek-reasoner → V4 Flash(官方兼容别名) + if strings.Contains(modelLower, "deepseek-chat") || strings.Contains(modelLower, "deepseek-reasoner") { + return s.fallbackPrices["deepseek-v4-flash"] + } + // OpenAI 仅匹配已知 GPT-5/Codex 族,避免未知 OpenAI 型号误计价。 if normalized := normalizeKnownOpenAICodexModel(modelLower); normalized != "" { switch normalized { diff --git a/backend/internal/service/billing_service_test.go b/backend/internal/service/billing_service_test.go index 52adc31c85..d75bbfde35 100644 --- a/backend/internal/service/billing_service_test.go +++ b/backend/internal/service/billing_service_test.go @@ -352,6 +352,10 @@ func TestGetFallbackPricing_FamilyMatching(t *testing.T) { {name: "openai legacy gpt5.1 codex falls back to gpt5.3 codex", model: "gpt-5.1-codex", expectedInput: 1.5e-6}, {name: "openai legacy codex mini latest falls back to gpt5.3 codex", model: "codex-mini-latest", expectedInput: 1.5e-6}, {name: "openai unknown no fallback", model: "gpt-unknown-model", expectNilPricing: true}, + {name: "deepseek v4 pro", model: "deepseek-v4-pro", expectedInput: 4.35e-7}, + {name: "deepseek v4 flash", model: "deepseek-v4-flash", expectedInput: 1.4e-7}, + {name: "deepseek chat alias → flash", model: "deepseek-chat", expectedInput: 1.4e-7}, + {name: "deepseek reasoner alias → flash", model: "deepseek-reasoner", expectedInput: 1.4e-7}, {name: "non supported family", model: "qwen-max", expectNilPricing: true}, } From 27e26a3a90a52b06ea9f1c64488c153db2183410 Mon Sep 17 00:00:00 2001 From: alfadb Date: Sat, 2 May 2026 11:26:41 +0800 Subject: [PATCH 2/8] chore: fix gofmt alignment --- backend/internal/service/billing_service.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/internal/service/billing_service.go b/backend/internal/service/billing_service.go index c4e376fcff..bdeef9185b 100644 --- a/backend/internal/service/billing_service.go +++ b/backend/internal/service/billing_service.go @@ -297,18 +297,18 @@ func (s *BillingService) initFallbackPricing() { // 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 + 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 + 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, } From 5a593a511e448af9dd4304a9956b81d7f1e2aa7a Mon Sep 17 00:00:00 2001 From: alfadb Date: Sat, 9 May 2026 21:13:22 +0800 Subject: [PATCH 3/8] test(billing): tighten DeepSeek V4 fallback assertions; clarify branch comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address copilot-pull-request-reviewer feedback on #2157: - billing_service_test.go: extend TestGetFallbackPricing_FamilyMatching with optional expectedOutput / expectedCacheRead fields and assert full Input/Output/CacheRead pricing for all 4 DeepSeek cases (v4-pro, v4-flash, deepseek-chat→flash, deepseek-reasoner→flash), preventing silent regression to 0 output/cache cost. - billing_service.go: rewrite the DeepSeek block comment to explicitly describe its scope (V4 Pro/Flash + chat/reasoner aliases, no unknown-deepseek fallback) and tighten the OpenAI comment to make it unambiguous that it only describes the OpenAI/Codex branch immediately below it. --- backend/internal/service/billing_service.go | 6 +-- .../internal/service/billing_service_test.go | 51 ++++++++++++++++--- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/backend/internal/service/billing_service.go b/backend/internal/service/billing_service.go index bdeef9185b..bbe9e97272 100644 --- a/backend/internal/service/billing_service.go +++ b/backend/internal/service/billing_service.go @@ -362,19 +362,19 @@ func (s *BillingService) getFallbackPricing(model string) *ModelPricing { return s.fallbackPrices["gemini-3.1-pro"] } - // DeepSeek V4 系列 + // DeepSeek V4 系列:仅匹配已知 V4 Pro/Flash 与官方兼容别名 + // (deepseek-chat / deepseek-reasoner → V4 Flash),未知 deepseek-* 型号不回退,避免误计价。 if strings.Contains(modelLower, "deepseek-v4-flash") { return s.fallbackPrices["deepseek-v4-flash"] } if strings.Contains(modelLower, "deepseek-v4-pro") { return s.fallbackPrices["deepseek-v4-pro"] } - // deepseek-chat / deepseek-reasoner → V4 Flash(官方兼容别名) if strings.Contains(modelLower, "deepseek-chat") || strings.Contains(modelLower, "deepseek-reasoner") { return s.fallbackPrices["deepseek-v4-flash"] } - // OpenAI 仅匹配已知 GPT-5/Codex 族,避免未知 OpenAI 型号误计价。 + // OpenAI(GPT-5 / Codex 族):仅匹配已知型号,避免未知 OpenAI 型号误计价。 if normalized := normalizeKnownOpenAICodexModel(modelLower); normalized != "" { switch normalized { case "gpt-5.5": diff --git a/backend/internal/service/billing_service_test.go b/backend/internal/service/billing_service_test.go index d75bbfde35..3857715b07 100644 --- a/backend/internal/service/billing_service_test.go +++ b/backend/internal/service/billing_service_test.go @@ -332,11 +332,14 @@ func TestCalculateCost_LongContextAppliesMultiplierToCacheCreation5mAnd1h(t *tes func TestGetFallbackPricing_FamilyMatching(t *testing.T) { svc := newTestBillingService() + // expectedOutput / expectedCacheRead 为 0 时跳过该字段断言(保持与原有用例兼容)。 tests := []struct { - name string - model string - expectedInput float64 - expectNilPricing bool + name string + model string + expectedInput float64 + expectedOutput float64 + expectedCacheRead float64 + expectNilPricing bool }{ {name: "empty model", model: " ", expectNilPricing: true}, {name: "claude opus 4.6", model: "claude-opus-4.6-20260201", expectedInput: 5e-6}, @@ -352,10 +355,34 @@ func TestGetFallbackPricing_FamilyMatching(t *testing.T) { {name: "openai legacy gpt5.1 codex falls back to gpt5.3 codex", model: "gpt-5.1-codex", expectedInput: 1.5e-6}, {name: "openai legacy codex mini latest falls back to gpt5.3 codex", model: "codex-mini-latest", expectedInput: 1.5e-6}, {name: "openai unknown no fallback", model: "gpt-unknown-model", expectNilPricing: true}, - {name: "deepseek v4 pro", model: "deepseek-v4-pro", expectedInput: 4.35e-7}, - {name: "deepseek v4 flash", model: "deepseek-v4-flash", expectedInput: 1.4e-7}, - {name: "deepseek chat alias → flash", model: "deepseek-chat", expectedInput: 1.4e-7}, - {name: "deepseek reasoner alias → flash", model: "deepseek-reasoner", expectedInput: 1.4e-7}, + { + name: "deepseek v4 pro", + model: "deepseek-v4-pro", + expectedInput: 4.35e-7, + expectedOutput: 8.7e-7, + expectedCacheRead: 3.625e-9, + }, + { + name: "deepseek v4 flash", + model: "deepseek-v4-flash", + expectedInput: 1.4e-7, + expectedOutput: 2.8e-7, + expectedCacheRead: 2.8e-9, + }, + { + name: "deepseek chat alias → flash", + model: "deepseek-chat", + expectedInput: 1.4e-7, + expectedOutput: 2.8e-7, + expectedCacheRead: 2.8e-9, + }, + { + name: "deepseek reasoner alias → flash", + model: "deepseek-reasoner", + expectedInput: 1.4e-7, + expectedOutput: 2.8e-7, + expectedCacheRead: 2.8e-9, + }, {name: "non supported family", model: "qwen-max", expectNilPricing: true}, } @@ -368,6 +395,14 @@ 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, + "OutputPricePerToken mismatch for %s", tt.model) + } + if tt.expectedCacheRead != 0 { + require.InDelta(t, tt.expectedCacheRead, pricing.CacheReadPricePerToken, 1e-14, + "CacheReadPricePerToken mismatch for %s", tt.model) + } }) } } From f597d98bdf343bcd4c682c20fcd070cc9af51560 Mon Sep 17 00:00:00 2001 From: alfadb Date: Fri, 15 May 2026 09:31:04 +0800 Subject: [PATCH 4/8] test(openai): use unpriced model in usage test --- .../internal/service/openai_gateway_record_usage_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/internal/service/openai_gateway_record_usage_test.go b/backend/internal/service/openai_gateway_record_usage_test.go index 1683fa4279..1e4e58dc42 100644 --- a/backend/internal/service/openai_gateway_record_usage_test.go +++ b/backend/internal/service/openai_gateway_record_usage_test.go @@ -327,7 +327,7 @@ func TestOpenAIGatewayServiceRecordUsage_MissingPricingRecordsZeroCostUsageLog(t InputTokens: 1200, OutputTokens: 300, }, - Model: "deepseek-v4-flash", + Model: "pricing-missing-test-model", Duration: time.Second, }, APIKey: &APIKey{ID: 1002, Quota: 100, Group: &Group{RateMultiplier: 1}}, @@ -346,8 +346,8 @@ func TestOpenAIGatewayServiceRecordUsage_MissingPricingRecordsZeroCostUsageLog(t require.NotNil(t, usageRepo.lastLog) require.Equal(t, "resp_missing_pricing", usageRepo.lastLog.RequestID) - require.Equal(t, "deepseek-v4-flash", usageRepo.lastLog.Model) - require.Equal(t, "deepseek-v4-flash", usageRepo.lastLog.RequestedModel) + require.Equal(t, "pricing-missing-test-model", usageRepo.lastLog.Model) + require.Equal(t, "pricing-missing-test-model", usageRepo.lastLog.RequestedModel) require.Equal(t, 1200, usageRepo.lastLog.InputTokens) require.Equal(t, 300, usageRepo.lastLog.OutputTokens) require.Zero(t, usageRepo.lastLog.TotalCost) From a4ce73391b1491f0e88291425dc5cbbe2af910c1 Mon Sep 17 00:00:00 2001 From: alfadb Date: Fri, 12 Jun 2026 10:07:35 +0800 Subject: [PATCH 5/8] feat(billing): add GLM / Kimi / MiniMax fallback pricing for Chinese LLM providers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 fix/deepseek-fallback-pricing 的 DeepSeek V4 Pro/Flash 定价收编, 并扩展国产 LLM 兜底定价覆盖: - 智谱 GLM (z.ai 公开 SKU 13 个): glm-5.1 / glm-5 / glm-5-turbo / glm-4.7 / glm-4.7-flashx / glm-4.6 / glm-4.5 / glm-4.5-x / glm-4.5-air / glm-4.5-airx / glm-4-32b-0414-128k / glm-4.5-flash / glm-4.7-flash - 月之暗面 Kimi K 系列 4 个: kimi-k2.6 / kimi-k2.5 / kimi-k2-thinking / kimi-k2 (K2-0905/K2-0711 官方未保留定价,隐性回退到 kimi-k2) - MiniMax M 系列 6 个: minimax-m3 (≤512K) / minimax-m2.7 / minimax-m2.7-highspeed / minimax-m2.5 / minimax-m2.1 / minimax-m2 - DeepSeek V4 沿用原 fix/deepseek-fallback-pricing 的 entry 移植过来 所有定价数据来自各家官方定价页 (USD/MTok 口径),与现有 Claude/GPT 风格保持一致 (硬编码在 Go 代码里,admin channel 优先)。 匹配策略:长 key 优先 (glm-5.1 优先于 glm-5, k2.6 优先于 k2); 未列出的国产厂商 alias (qwen/doubao/hunyuan) 一律不返回兜底价, 避免误计价——这与 fix/deepseek-fallback-pricing 的白名单语义一致。 数据源注释 (Source URL) 内嵌在每个 initFallbackPricing 分组开头, 便于后续 PR 同步上游价格变化。 --- backend/internal/service/billing_service.go | 255 ++++++++++++++++++ .../internal/service/billing_service_test.go | 206 +++++++++++++- 2 files changed, 460 insertions(+), 1 deletion(-) diff --git a/backend/internal/service/billing_service.go b/backend/internal/service/billing_service.go index bbe9e97272..a1008e0515 100644 --- a/backend/internal/service/billing_service.go +++ b/backend/internal/service/billing_service.go @@ -323,6 +323,180 @@ func (s *BillingService) initFallbackPricing() { CacheReadPricePerTokenPriority: 0.3e-6, SupportsCacheBreakdown: false, } + + // ============================================================ + // 国产 LLM 兜底定价(数据源:各家官方定价页/USD 口径) + // 顺序:DeepSeek → 智谱 GLM → 月之暗面 Kimi → MiniMax + // 覆盖逻辑见同文件 getFallbackPricing() + // ============================================================ + + // ---- DeepSeek V4 系列 ---- + // Source: https://api-docs.deepseek.com/quick_start/pricing + // (deepseek-chat / deepseek-reasoner 为 deepseek-v4-flash 的兼容别名,2026/07/24 弃用) + s.fallbackPrices["deepseek-v4-pro"] = &ModelPricing{ + InputPricePerToken: 4.35e-7, // $0.435 per MTok (cache miss) + OutputPricePerToken: 8.7e-7, // $0.87 per MTok + CacheReadPricePerToken: 3.625e-9, // $0.003625 per MTok (cache hit) + SupportsCacheBreakdown: false, + } + s.fallbackPrices["deepseek-v4-flash"] = &ModelPricing{ + InputPricePerToken: 1.4e-7, // $0.14 per MTok (cache miss) + OutputPricePerToken: 2.8e-7, // $0.28 per MTok + CacheReadPricePerToken: 2.8e-9, // $0.0028 per MTok (cache hit) + SupportsCacheBreakdown: false, + } + + // ---- 智谱 GLM(Z.AI)---- + // Source: https://docs.z.ai/guides/overview/pricing (USD per 1M tokens) + // 注意:CacheReadPricePerToken 即"缓存命中"价格,CacheCreationPricePerToken 留空(智谱未公开写入价,按 0 处理)。 + // GLM-4.6 与 GLM-4.5 在 z.ai 国际版上定价一致;GLM-4.5 国内按 ¥0.8/¥2,汇率换算后约 $0.112/$0.28,与国际版 $0.6/$2.2 不同,本分支采用国际版 USD 口径与现有 Claude/GPT 一致。 + s.fallbackPrices["glm-5.1"] = &ModelPricing{ + InputPricePerToken: 1.4e-6, // $1.40 per MTok + OutputPricePerToken: 4.4e-6, // $4.40 per MTok + CacheReadPricePerToken: 0.26e-6, + SupportsCacheBreakdown: false, + } + s.fallbackPrices["glm-5"] = &ModelPricing{ + InputPricePerToken: 1e-6, // $1.00 per MTok + OutputPricePerToken: 3.2e-6, + CacheReadPricePerToken: 0.2e-6, + SupportsCacheBreakdown: false, + } + s.fallbackPrices["glm-5-turbo"] = &ModelPricing{ + InputPricePerToken: 1.2e-6, + OutputPricePerToken: 4e-6, + CacheReadPricePerToken: 0.24e-6, + SupportsCacheBreakdown: false, + } + s.fallbackPrices["glm-4.7"] = &ModelPricing{ + InputPricePerToken: 0.6e-6, // $0.60 per MTok + OutputPricePerToken: 2.2e-6, + CacheReadPricePerToken: 0.11e-6, + SupportsCacheBreakdown: false, + } + s.fallbackPrices["glm-4.7-flashx"] = &ModelPricing{ + InputPricePerToken: 0.07e-6, // $0.07 per MTok + OutputPricePerToken: 0.4e-6, + CacheReadPricePerToken: 0.01e-6, + SupportsCacheBreakdown: false, + } + s.fallbackPrices["glm-4.6"] = &ModelPricing{ + InputPricePerToken: 0.6e-6, // $0.60 per MTok + OutputPricePerToken: 2.2e-6, + CacheReadPricePerToken: 0.11e-6, + SupportsCacheBreakdown: false, + } + s.fallbackPrices["glm-4.5"] = &ModelPricing{ + InputPricePerToken: 0.6e-6, // $0.60 per MTok + OutputPricePerToken: 2.2e-6, + CacheReadPricePerToken: 0.11e-6, + SupportsCacheBreakdown: false, + } + s.fallbackPrices["glm-4.5-x"] = &ModelPricing{ + InputPricePerToken: 2.2e-6, // $2.20 per MTok + OutputPricePerToken: 8.9e-6, + CacheReadPricePerToken: 0.45e-6, + SupportsCacheBreakdown: false, + } + s.fallbackPrices["glm-4.5-air"] = &ModelPricing{ + InputPricePerToken: 0.2e-6, // $0.20 per MTok + OutputPricePerToken: 1.1e-6, + CacheReadPricePerToken: 0.03e-6, + SupportsCacheBreakdown: false, + } + s.fallbackPrices["glm-4.5-airx"] = &ModelPricing{ + InputPricePerToken: 1.1e-6, + OutputPricePerToken: 4.5e-6, + CacheReadPricePerToken: 0.22e-6, + SupportsCacheBreakdown: false, + } + s.fallbackPrices["glm-4-32b-0414-128k"] = &ModelPricing{ + InputPricePerToken: 0.1e-6, // $0.10 per MTok + OutputPricePerToken: 0.1e-6, + SupportsCacheBreakdown: false, + } + // GLM-4.5-Flash / GLM-4.7-Flash 在 z.ai 上为 Free,保留 zero-cost entry 防止未知 alias 误计费。 + s.fallbackPrices["glm-4.5-flash"] = &ModelPricing{ + InputPricePerToken: 0, + OutputPricePerToken: 0, + SupportsCacheBreakdown: false, + } + s.fallbackPrices["glm-4.7-flash"] = &ModelPricing{ + InputPricePerToken: 0, + OutputPricePerToken: 0, + SupportsCacheBreakdown: false, + } + + // ---- 月之暗面 Kimi(K 系列)---- + // Source: https://platform.moonshot.cn/docs/pricing/overview (元/百万 tokens 口径) + // 交叉验证:https://www.tmtpost.com/7961404.html (USD 口径) + // Moonshot V1 (¥2/¥5/¥10 多 tier) 公开页未直接标注 USD 价,本分支不覆盖,避免误计价。 + // K2-0905 / K2-0711 官方页面未保留定价,不覆盖。 + s.fallbackPrices["kimi-k2.6"] = &ModelPricing{ + InputPricePerToken: 0.95e-6, // $0.95 per MTok (cache miss) + OutputPricePerToken: 4e-6, // $4.00 per MTok + CacheReadPricePerToken: 0.15e-6, // $0.15 per MTok (cache hit, ¥1.10) + SupportsCacheBreakdown: false, + } + s.fallbackPrices["kimi-k2.5"] = &ModelPricing{ + InputPricePerToken: 0.60e-6, // $0.60 per MTok + OutputPricePerToken: 3e-6, // $3.00 per MTok + CacheReadPricePerToken: 0.098e-6, + SupportsCacheBreakdown: false, + } + s.fallbackPrices["kimi-k2-thinking"] = &ModelPricing{ + InputPricePerToken: 0.56e-6, // ¥4/百万 ≈ $0.56 + OutputPricePerToken: 2.24e-6, // ¥16/百万 + CacheReadPricePerToken: 0.14e-6, // ¥1/百万 + SupportsCacheBreakdown: false, + } + s.fallbackPrices["kimi-k2"] = &ModelPricing{ + InputPricePerToken: 0.56e-6, // ¥4/百万 + OutputPricePerToken: 2.24e-6, // ¥16/百万 + CacheReadPricePerToken: 0.14e-6, // ¥1/百万 + SupportsCacheBreakdown: false, + } + + // ---- MiniMax M 系列 ---- + // Source: https://platform.minimax.io/docs/guides/pricing-paygo + // 注意:MiniMax M3 在 >512K context 时价格翻倍,本兜底采用 ≤512K 标准 tier(保守口径,对用户有利)。 + // 如需支持长上下文 multiplier,可后续参考 GPT-5.4 模式扩展 LongContextXxx 字段。 + s.fallbackPrices["minimax-m3"] = &ModelPricing{ + InputPricePerToken: 0.60e-6, // $0.60 per MTok (≤512K standard tier, 含 50% 永久折扣前原价 $1.20) + OutputPricePerToken: 2.40e-6, + CacheReadPricePerToken: 0.12e-6, + SupportsCacheBreakdown: false, + } + s.fallbackPrices["minimax-m2.7"] = &ModelPricing{ + InputPricePerToken: 0.30e-6, // $0.30 per MTok + OutputPricePerToken: 1.20e-6, + CacheReadPricePerToken: 0.06e-6, + SupportsCacheBreakdown: false, + } + s.fallbackPrices["minimax-m2.7-highspeed"] = &ModelPricing{ + InputPricePerToken: 0.60e-6, + OutputPricePerToken: 2.40e-6, + CacheReadPricePerToken: 0.06e-6, + SupportsCacheBreakdown: false, + } + s.fallbackPrices["minimax-m2.5"] = &ModelPricing{ + InputPricePerToken: 0.30e-6, + OutputPricePerToken: 1.20e-6, + CacheReadPricePerToken: 0.03e-6, + SupportsCacheBreakdown: false, + } + s.fallbackPrices["minimax-m2.1"] = &ModelPricing{ + InputPricePerToken: 0.30e-6, + OutputPricePerToken: 1.20e-6, + CacheReadPricePerToken: 0.03e-6, + SupportsCacheBreakdown: false, + } + s.fallbackPrices["minimax-m2"] = &ModelPricing{ + InputPricePerToken: 0.30e-6, + OutputPricePerToken: 1.20e-6, + CacheReadPricePerToken: 0.03e-6, + SupportsCacheBreakdown: false, + } } // getFallbackPricing 根据模型系列获取回退价格 @@ -374,6 +548,87 @@ func (s *BillingService) getFallbackPricing(model string) *ModelPricing { return s.fallbackPrices["deepseek-v4-flash"] } + // ---- 国产 LLM 兜底匹配 ---- + // 匹配策略:长 key 优先(具体模型 → 系列 / 厂商),未知型号不回退以避免误计价。 + // 与 DeepSeek 一样采用"白名单"语义:未在本表命中的国产模型 alias 一律不返回兜底价。 + + // 智谱 GLM(z.ai 公开 SKU:glm-5.1 / glm-5 / glm-5-turbo / glm-4.7 / glm-4.6 / glm-4.5 等) + // 匹配顺序:先判别最高 tier,再依次降级。 + if strings.Contains(modelLower, "glm-5.1") { + return s.fallbackPrices["glm-5.1"] + } + if strings.Contains(modelLower, "glm-5-turbo") || strings.Contains(modelLower, "glm-5turbo") { + return s.fallbackPrices["glm-5-turbo"] + } + if strings.Contains(modelLower, "glm-5") { + return s.fallbackPrices["glm-5"] + } + if strings.Contains(modelLower, "glm-4.7-flashx") { + return s.fallbackPrices["glm-4.7-flashx"] + } + if strings.Contains(modelLower, "glm-4.7-flash") { + return s.fallbackPrices["glm-4.7-flash"] + } + if strings.Contains(modelLower, "glm-4.7") { + return s.fallbackPrices["glm-4.7"] + } + if strings.Contains(modelLower, "glm-4.6") { + return s.fallbackPrices["glm-4.6"] + } + if strings.Contains(modelLower, "glm-4.5-flash") { + return s.fallbackPrices["glm-4.5-flash"] + } + if strings.Contains(modelLower, "glm-4.5-x") || strings.Contains(modelLower, "glm-4.5x") { + return s.fallbackPrices["glm-4.5-x"] + } + if strings.Contains(modelLower, "glm-4.5-airx") || strings.Contains(modelLower, "glm-4.5airx") { + return s.fallbackPrices["glm-4.5-airx"] + } + if strings.Contains(modelLower, "glm-4.5-air") || strings.Contains(modelLower, "glm-4.5air") { + return s.fallbackPrices["glm-4.5-air"] + } + if strings.Contains(modelLower, "glm-4.5") { + return s.fallbackPrices["glm-4.5"] + } + if strings.Contains(modelLower, "glm-4-32b") { + return s.fallbackPrices["glm-4-32b-0414-128k"] + } + + // 月之暗面 Kimi(kimi-k2.6 / kimi-k2.5 / kimi-k2-thinking / kimi-k2) + // K2-0905 / K2-0711 官方未保留定价,不进入 fallback。 + if strings.Contains(modelLower, "kimi-k2.6") || strings.Contains(modelLower, "kimi-k2-6") { + return s.fallbackPrices["kimi-k2.6"] + } + if strings.Contains(modelLower, "kimi-k2.5") || strings.Contains(modelLower, "kimi-k2-5") { + return s.fallbackPrices["kimi-k2.5"] + } + if strings.Contains(modelLower, "kimi-k2-thinking") || strings.Contains(modelLower, "kimi-k2-thinking-") { + return s.fallbackPrices["kimi-k2-thinking"] + } + if strings.Contains(modelLower, "kimi-k2") || strings.Contains(modelLower, "kimi/k2") { + return s.fallbackPrices["kimi-k2"] + } + + // MiniMax M 系列(M3 / M2.7 / M2.5 / M2.1 / M2;含 highspeed 变体) + if strings.Contains(modelLower, "minimax-m3") || 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") { + return s.fallbackPrices["minimax-m2.7-highspeed"] + } + if strings.Contains(modelLower, "minimax-m2.7") || strings.Contains(modelLower, "minimax-m2-7") { + return s.fallbackPrices["minimax-m2.7"] + } + if strings.Contains(modelLower, "minimax-m2.5") || strings.Contains(modelLower, "minimax-m2-5") { + return s.fallbackPrices["minimax-m2.5"] + } + if strings.Contains(modelLower, "minimax-m2.1") || strings.Contains(modelLower, "minimax-m2-1") { + return s.fallbackPrices["minimax-m2.1"] + } + if strings.Contains(modelLower, "minimax-m2") || strings.Contains(modelLower, "minimax-m-2") { + return s.fallbackPrices["minimax-m2"] + } + // OpenAI(GPT-5 / Codex 族):仅匹配已知型号,避免未知 OpenAI 型号误计价。 if normalized := normalizeKnownOpenAICodexModel(modelLower); normalized != "" { switch normalized { diff --git a/backend/internal/service/billing_service_test.go b/backend/internal/service/billing_service_test.go index 3857715b07..4935ad8487 100644 --- a/backend/internal/service/billing_service_test.go +++ b/backend/internal/service/billing_service_test.go @@ -383,7 +383,211 @@ func TestGetFallbackPricing_FamilyMatching(t *testing.T) { expectedOutput: 2.8e-7, expectedCacheRead: 2.8e-9, }, - {name: "non supported family", model: "qwen-max", expectNilPricing: true}, + + // ---- 智谱 GLM(z.ai USD 口径)---- + { + name: "glm 5.1 flagship", + model: "glm-5.1", + expectedInput: 1.4e-6, + expectedOutput: 4.4e-6, + expectedCacheRead: 0.26e-6, + }, + { + name: "glm 5 base", + model: "glm-5", + expectedInput: 1e-6, + expectedOutput: 3.2e-6, + expectedCacheRead: 0.2e-6, + }, + { + name: "glm 5 turbo", + model: "glm-5-turbo", + expectedInput: 1.2e-6, + expectedOutput: 4e-6, + expectedCacheRead: 0.24e-6, + }, + { + name: "glm 4.7", + model: "glm-4.7", + expectedInput: 0.6e-6, + expectedOutput: 2.2e-6, + expectedCacheRead: 0.11e-6, + }, + { + name: "glm 4.6", + model: "glm-4.6", + expectedInput: 0.6e-6, + expectedOutput: 2.2e-6, + expectedCacheRead: 0.11e-6, + }, + { + name: "glm 4.5", + model: "glm-4.5", + expectedInput: 0.6e-6, + expectedOutput: 2.2e-6, + expectedCacheRead: 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, + }, + { + name: "glm 4.5-air lightweight", + model: "glm-4.5-air", + expectedInput: 0.2e-6, + expectedOutput: 1.1e-6, + expectedCacheRead: 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, + }, + { + name: "glm 4.5-flash free tier", + model: "glm-4.5-flash", + expectedInput: 0, // Free tier on z.ai + expectedOutput: 0, + expectedCacheRead: 0, + }, + { + name: "glm 4.7-flash free tier", + model: "glm-4.7-flash", + expectedInput: 0, + expectedOutput: 0, + expectedCacheRead: 0, + }, + { + name: "glm 4-32b legacy", + model: "glm-4-32b-0414-128k", + expectedInput: 0.1e-6, + expectedOutput: 0.1e-6, + expectedCacheRead: 0, + }, + // 关键: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, + }, + { + 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, + }, + + // ---- 月之暗面 Kimi ---- + { + name: "kimi k2.6 flagship", + model: "kimi-k2.6", + expectedInput: 0.95e-6, + expectedOutput: 4e-6, + expectedCacheRead: 0.15e-6, + }, + { + name: "kimi k2.5", + model: "kimi-k2.5", + expectedInput: 0.60e-6, + expectedOutput: 3e-6, + expectedCacheRead: 0.098e-6, + }, + { + name: "kimi k2-thinking", + model: "kimi-k2-thinking", + expectedInput: 0.56e-6, + expectedOutput: 2.24e-6, + expectedCacheRead: 0.14e-6, + }, + { + name: "kimi k2 base", + model: "kimi-k2", + expectedInput: 0.56e-6, + expectedOutput: 2.24e-6, + expectedCacheRead: 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, + }, + { + name: "kimi k2 thinking hyphenated variant", + model: "kimi-k2-thinking-preview", + expectedInput: 0.56e-6, + expectedOutput: 2.24e-6, + expectedCacheRead: 0.14e-6, + }, + + // ---- MiniMax M 系列 ---- + { + name: "minimax m3", + model: "minimax-m3", + expectedInput: 0.60e-6, + expectedOutput: 2.40e-6, + expectedCacheRead: 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, + }, + { + name: "minimax m2.7", + model: "minimax-m2.7", + expectedInput: 0.30e-6, + expectedOutput: 1.20e-6, + expectedCacheRead: 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, + }, + { + name: "minimax m2.5", + model: "minimax-m2.5", + expectedInput: 0.30e-6, + expectedOutput: 1.20e-6, + expectedCacheRead: 0.03e-6, + }, + { + name: "minimax m2 legacy", + model: "minimax-m2", + expectedInput: 0.30e-6, + expectedOutput: 1.20e-6, + expectedCacheRead: 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}, + {name: "moonshot v1 not covered", model: "moonshot-v1-8k", expectNilPricing: true}, + // kimi-k2-0905 / kimi-k2-0711 官方未公布独立价,走 kimi-k2 隐性回退(接受)—— + // 如未来官方公布独立价,需在 getFallbackPricing 加显式分支。 + { + 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, + }, } for _, tt := range tests { From c90089c8143d9cee3c0291316d89dde0590ea75c Mon Sep 17 00:00:00 2001 From: alfadb Date: Fri, 12 Jun 2026 10:29:24 +0800 Subject: [PATCH 6/8] 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) } }) From 4f5f2788e1f9b75c193c5eb05b2d229447bd3cdf Mon Sep 17 00:00:00 2001 From: alfadb Date: Fri, 12 Jun 2026 13:38:19 +0800 Subject: [PATCH 7/8] fix(billing): add kimi-for-coding fallback pricing --- backend/internal/service/billing_service.go | 12 +++++++++++- backend/internal/service/billing_service_test.go | 7 +++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/backend/internal/service/billing_service.go b/backend/internal/service/billing_service.go index 43e91ce3e2..da36ebdaad 100644 --- a/backend/internal/service/billing_service.go +++ b/backend/internal/service/billing_service.go @@ -420,6 +420,13 @@ func (s *BillingService) initFallbackPricing() { CacheReadPricePerToken: 0.15e-6, // $0.15 per MTok (cache hit, ¥1.10) SupportsCacheBreakdown: false, } + // kimi-for-coding 走 Kimi Coding endpoint,按当前 K2.6 coding 档位兜底计费。 + s.fallbackPrices["kimi-for-coding"] = &ModelPricing{ + InputPricePerToken: 0.95e-6, + OutputPricePerToken: 4e-6, + CacheReadPricePerToken: 0.15e-6, + SupportsCacheBreakdown: false, + } s.fallbackPrices["kimi-k2.5"] = &ModelPricing{ InputPricePerToken: 0.60e-6, // $0.60 per MTok OutputPricePerToken: 3e-6, // $3.00 per MTok @@ -576,8 +583,11 @@ func (s *BillingService) getFallbackPricing(model string) *ModelPricing { return s.fallbackPrices["glm-4-32b-0414-128k"] } - // 月之暗面 Kimi(kimi-k2.6 / kimi-k2.5 / kimi-k2-thinking / kimi-k2) + // 月之暗面 Kimi(kimi-k2.6 / kimi-for-coding / kimi-k2.5 / kimi-k2-thinking / kimi-k2) // K2-0905 / K2-0711 官方未保留定价,不进入 fallback。 + if strings.Contains(modelLower, "kimi-for-coding") { + return s.fallbackPrices["kimi-for-coding"] + } if strings.Contains(modelLower, "kimi-k2.6") || strings.Contains(modelLower, "kimi-k2-6") { return s.fallbackPrices["kimi-k2.6"] } diff --git a/backend/internal/service/billing_service_test.go b/backend/internal/service/billing_service_test.go index 8bd6e41ede..45df3cd6bf 100644 --- a/backend/internal/service/billing_service_test.go +++ b/backend/internal/service/billing_service_test.go @@ -494,6 +494,13 @@ func TestGetFallbackPricing_FamilyMatching(t *testing.T) { expectedOutput: floatPtr(4e-6), expectedCacheRead: floatPtr(0.15e-6), }, + { + name: "kimi for coding explicit alias", + model: "kimi-for-coding", + expectedInput: 0.95e-6, + expectedOutput: floatPtr(4e-6), + expectedCacheRead: floatPtr(0.15e-6), + }, { name: "kimi k2.5", model: "kimi-k2.5", From 262fe1230d923f4f3e33a1ad3ace1e2f20a1693e Mon Sep 17 00:00:00 2001 From: alfadb Date: Mon, 15 Jun 2026 12:02:06 +0800 Subject: [PATCH 8/8] =?UTF-8?q?feat(billing):=20=E4=B8=BA=20doubao-embeddi?= =?UTF-8?q?ng-vision=20=E6=B7=BB=E5=8A=A0=E5=9B=BE=E6=96=87=E5=B7=AE?= =?UTF-8?q?=E5=88=AB=E5=85=9C=E5=BA=95=E5=AE=9A=E4=BB=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 火山方舟 doubao-embedding-vision 多模态向量化按量付费官方价为文本 ¥0.7/MTok、图片 ¥1.8/MTok,二者不同价。原计费引擎对 embedding 仅记单一 input token、无图片输入档位,无法表达该差别。 变更: - ModelPricing 新增 ImageInputPricePerToken 字段 - OpenAIUsage / UsageTokens 新增 ImageInputTokens 字段 - extractOpenAIEmbeddingsUsage 解析 usage.prompt_tokens_details.image_tokens - CalculateCost 拆分文本/图片输入计费;ImageInputTokens 为 0 时走原单价路径,存量 chat/vision 流量行为不变 - getFallbackPricing 新增 doubao-embedding-vision 分支(most-specific-first,覆盖带版本后缀别名),fallback 表填入 $0.098/$0.252 per MTok(汇率 ÷7.14) 测试:新增图文混合/纯文本/图片 token 超额三类用例及定价回退断言;service 包单测与 golangci-lint 全通过。 --- backend/internal/service/billing_service.go | 39 +++++++++++- .../internal/service/billing_service_test.go | 61 +++++++++++++++++++ backend/internal/service/openai_embeddings.go | 7 +++ .../service/openai_gateway_service.go | 2 + 4 files changed, 108 insertions(+), 1 deletion(-) diff --git a/backend/internal/service/billing_service.go b/backend/internal/service/billing_service.go index da36ebdaad..74055a8151 100644 --- a/backend/internal/service/billing_service.go +++ b/backend/internal/service/billing_service.go @@ -91,6 +91,7 @@ type BillingCache interface { type ModelPricing struct { InputPricePerToken float64 // 每token输入价格 (USD) InputPricePerTokenPriority float64 // priority service tier 下每token输入价格 (USD) + ImageInputPricePerToken float64 // 图片输入 token 价格 (USD),用于多模态 embedding 等图文不同价场景;为 0 时回退到 InputPricePerToken OutputPricePerToken float64 // 每token输出价格 (USD) OutputPricePerTokenPriority float64 // priority service tier 下每token输出价格 (USD) CacheCreationPricePerToken float64 // 缓存创建每token价格 (USD) @@ -137,6 +138,7 @@ func serviceTierCostMultiplier(serviceTier string) float64 { // UsageTokens 使用的token数量 type UsageTokens struct { InputTokens int + ImageInputTokens int OutputTokens int CacheCreationTokens int CacheReadTokens int @@ -486,6 +488,17 @@ func (s *BillingService) initFallbackPricing() { CacheReadPricePerToken: 0.03e-6, SupportsCacheBreakdown: false, } + + // ---- 火山方舟 豆包 Embedding(多模态向量化)---- + // doubao-embedding-vision 图文向量化:上游 usage 回传 prompt_tokens_details.{text_tokens,image_tokens}, + // 按量付费官方价 文本 ¥0.7/MTok、图片 ¥1.8/MTok;汇率口径 ÷7.14(与本表其他国产模型一致,¥1≈$0.14)。 + // embedding 无 output,OutputPricePerToken 置 0。 + s.fallbackPrices["doubao-embedding-vision"] = &ModelPricing{ + InputPricePerToken: 0.098e-6, // ¥0.7/MTok ≈ $0.098(文本输入) + ImageInputPricePerToken: 0.252e-6, // ¥1.8/MTok ≈ $0.252(图片输入) + OutputPricePerToken: 0, + SupportsCacheBreakdown: false, + } } // getFallbackPricing 根据模型系列获取回退价格 @@ -621,6 +634,13 @@ func (s *BillingService) getFallbackPricing(model string) *ModelPricing { return s.fallbackPrices["minimax-m2"] } + // 火山方舟 豆包 Embedding(多模态向量化)。 + // most-specific-first:放在未来任何 doubao-embedding / doubao 宽匹配之前。 + // 覆盖带版本后缀的别名(如 doubao-embedding-vision-251215)。 + if strings.Contains(modelLower, "doubao-embedding-vision") { + return s.fallbackPrices["doubao-embedding-vision"] + } + // OpenAI(GPT-5 / Codex 族):仅匹配已知型号,避免未知 OpenAI 型号误计价。 if normalized := normalizeKnownOpenAICodexModel(modelLower); normalized != "" { switch normalized { @@ -839,7 +859,24 @@ func (s *BillingService) computeTokenBreakdown( } bd := &CostBreakdown{} - bd.InputCost = float64(tokens.InputTokens) * inputPrice + // 分离图片输入 token 与文本输入 token(多模态 embedding 等图文不同价场景)。 + // ImageInputTokens 为 0 时(绝大多数 chat/vision 流量)走原始单价路径,行为不变。 + if tokens.ImageInputTokens > 0 { + imageInputTokens := tokens.ImageInputTokens + textInputTokens := tokens.InputTokens - imageInputTokens + if textInputTokens < 0 { + textInputTokens = 0 + imageInputTokens = tokens.InputTokens + } + imageInputPrice := pricing.ImageInputPricePerToken + if imageInputPrice == 0 { + // 未配置图片输入档时回退到文本 input 价(已含 priority / 长上下文调整) + imageInputPrice = inputPrice + } + bd.InputCost = float64(textInputTokens)*inputPrice + float64(imageInputTokens)*imageInputPrice + } else { + bd.InputCost = float64(tokens.InputTokens) * inputPrice + } // 分离图片输出 token 与文本输出 token textOutputTokens := tokens.OutputTokens - tokens.ImageOutputTokens diff --git a/backend/internal/service/billing_service_test.go b/backend/internal/service/billing_service_test.go index 45df3cd6bf..9541047d25 100644 --- a/backend/internal/service/billing_service_test.go +++ b/backend/internal/service/billing_service_test.go @@ -582,9 +582,24 @@ func TestGetFallbackPricing_FamilyMatching(t *testing.T) { expectedCacheRead: floatPtr(0.03e-6), }, + // ---- 火山方舟 豆包 Embedding(多模态向量化)---- + { + name: "doubao embedding vision text rate", + model: "doubao-embedding-vision", + expectedInput: 0.098e-6, + expectedOutput: floatPtr(0), + }, + { + name: "doubao embedding vision versioned alias", + model: "doubao-embedding-vision-251215", + expectedInput: 0.098e-6, + }, + // ---- 负向用例 ---- {name: "qwen unknown no fallback", model: "qwen-max", expectNilPricing: true}, + // doubao-pro / doubao-embedding(纯文本)不在白名单,不回退;仅 doubao-embedding-vision 显式命中。 {name: "doubao unknown no fallback", model: "doubao-pro", expectNilPricing: true}, + {name: "doubao text embedding no fallback", model: "doubao-embedding-text-240515", expectNilPricing: true}, {name: "hunyuan unknown no fallback", model: "hunyuan-t1", expectNilPricing: true}, {name: "moonshot v1 not covered", model: "moonshot-v1-8k", expectNilPricing: true}, // kimi-k2-0905 / kimi-k2-0711 官方未公布独立价,走 kimi-k2 隐性回退(接受)—— @@ -618,6 +633,52 @@ func TestGetFallbackPricing_FamilyMatching(t *testing.T) { }) } } + +// doubao-embedding-vision 是首个图文不同价的 embedding:文本 ¥0.7/MTok、图片 ¥1.8/MTok。 +// 验证回退表同时携带文本与图片两档单价,且能被带版本后缀 / 大小写别名命中。 +func TestGetModelPricing_DoubaoEmbeddingVisionImageInputRate(t *testing.T) { + svc := newTestBillingService() + + for _, model := range []string{ + "doubao-embedding-vision", + "doubao-embedding-vision-251215", + "Doubao-Embedding-Vision", + } { + pricing, err := svc.GetModelPricing(model) + require.NoError(t, err, "model %s should resolve fallback pricing", model) + require.NotNil(t, pricing) + require.InDelta(t, 0.098e-6, pricing.InputPricePerToken, 1e-12, "text input rate for %s", model) + require.InDelta(t, 0.252e-6, pricing.ImageInputPricePerToken, 1e-12, "image input rate for %s", model) + require.Zero(t, pricing.OutputPricePerToken, "embedding has no output cost for %s", model) + } +} + +// 验证双档计费:InputCost = 文本token×文本价 + 图片token×图片价; +// 且 ImageInputTokens=0 时走原单价路径,ImageInputTokens>InputTokens 时不负计文本。 +func TestCalculateCost_DoubaoEmbeddingVisionDifferentialInput(t *testing.T) { + svc := newTestBillingService() + + // 图文混合:prompt_tokens=1340,其中 image_tokens=28、text_tokens=1312。 + mixed := UsageTokens{InputTokens: 1340, ImageInputTokens: 28} + cost, err := svc.CalculateCost("doubao-embedding-vision", mixed, 1.0) + require.NoError(t, err) + wantMixed := float64(1312)*0.098e-6 + float64(28)*0.252e-6 + require.InDelta(t, wantMixed, cost.InputCost, 1e-15) + require.InDelta(t, wantMixed, cost.TotalCost, 1e-15) + require.Zero(t, cost.OutputCost) + + // 纯文本:全部按文本档计费,与原单价路径一致。 + textOnly := UsageTokens{InputTokens: 1340} + costText, err := svc.CalculateCost("doubao-embedding-vision", textOnly, 1.0) + require.NoError(t, err) + require.InDelta(t, float64(1340)*0.098e-6, costText.InputCost, 1e-15) + + // 健壮性:ImageInputTokens 超过 InputTokens 时,文本置 0、计费 token 不超过 InputTokens。 + weird := UsageTokens{InputTokens: 10, ImageInputTokens: 50} + costWeird, err := svc.CalculateCost("doubao-embedding-vision", weird, 1.0) + require.NoError(t, err) + require.InDelta(t, float64(10)*0.252e-6, costWeird.InputCost, 1e-15) +} func TestCalculateCostWithLongContext_BelowThreshold(t *testing.T) { svc := newTestBillingService() diff --git a/backend/internal/service/openai_embeddings.go b/backend/internal/service/openai_embeddings.go index 7c710259f3..0fb3fff1f7 100644 --- a/backend/internal/service/openai_embeddings.go +++ b/backend/internal/service/openai_embeddings.go @@ -214,8 +214,15 @@ func extractOpenAIEmbeddingsUsage(body []byte) OpenAIUsage { usage.Get("cache_creation_input_tokens"), usage.Get("input_tokens_details.cache_creation_tokens"), ) + // 多模态 embedding(如 doubao-embedding-vision)回传图文 token 拆分, + // 用于图文不同价计费;纯文本 embedding 该字段为 0,行为不变。 + imageInputTokens := firstPositiveGJSONInt( + usage.Get("prompt_tokens_details.image_tokens"), + usage.Get("input_tokens_details.image_tokens"), + ) return OpenAIUsage{ InputTokens: inputTokens, + ImageInputTokens: imageInputTokens, OutputTokens: outputTokens, CacheReadInputTokens: cacheReadTokens, CacheCreationInputTokens: cacheCreationTokens, diff --git a/backend/internal/service/openai_gateway_service.go b/backend/internal/service/openai_gateway_service.go index d056668a48..7fb53e2f55 100644 --- a/backend/internal/service/openai_gateway_service.go +++ b/backend/internal/service/openai_gateway_service.go @@ -212,6 +212,7 @@ func (s *OpenAICodexUsageSnapshot) Normalize() *NormalizedCodexLimits { // OpenAIUsage represents OpenAI API response usage type OpenAIUsage struct { InputTokens int `json:"input_tokens"` + ImageInputTokens int `json:"image_input_tokens,omitempty"` OutputTokens int `json:"output_tokens"` CacheCreationInputTokens int `json:"cache_creation_input_tokens,omitempty"` CacheReadInputTokens int `json:"cache_read_input_tokens,omitempty"` @@ -5912,6 +5913,7 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec // Calculate cost tokens := UsageTokens{ InputTokens: actualInputTokens, + ImageInputTokens: result.Usage.ImageInputTokens, OutputTokens: result.Usage.OutputTokens, CacheCreationTokens: result.Usage.CacheCreationInputTokens, CacheReadTokens: result.Usage.CacheReadInputTokens,