diff --git a/backend/internal/service/billing_service.go b/backend/internal/service/billing_service.go index 893c6d0c7..a43f26a8a 100644 --- a/backend/internal/service/billing_service.go +++ b/backend/internal/service/billing_service.go @@ -403,6 +403,13 @@ func (s *BillingService) initFallbackPricing() { // 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 一致。 + // GLM-5.2 与 GLM-5.1 在 z.ai 上同价。 + s.fallbackPrices["glm-5.2"] = &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.1"] = &ModelPricing{ InputPricePerToken: 1.4e-6, // $1.40 per MTok OutputPricePerToken: 4.4e-6, // $4.40 per MTok @@ -670,8 +677,12 @@ func (s *BillingService) getFallbackPricing(model string) *ModelPricing { // 匹配策略:长 key 优先(具体模型 → 系列 / 厂商),未知型号不回退以避免误计价。 // 与 DeepSeek 一样采用"白名单"语义:未在本表命中的国产模型 alias 一律不返回兜底价。 - // 智谱 GLM(z.ai 公开 SKU:glm-5.1 / glm-5 / glm-5-turbo / glm-4.7 / glm-4.6 / glm-4.5 等) + // 智谱 GLM(z.ai 公开 SKU:glm-5.2 / glm-5.1 / glm-5 / glm-5-turbo / glm-4.7 / glm-4.6 / glm-4.5 等) // 匹配顺序:先判别最高 tier,再依次降级。 + // 注意:带小数点的型号必须排在裸 "glm-5" 之前,否则会被 strings.Contains 抢走。 + if strings.Contains(modelLower, "glm-5.2") { + return s.fallbackPrices["glm-5.2"] + } if strings.Contains(modelLower, "glm-5.1") { return s.fallbackPrices["glm-5.1"] } diff --git a/backend/internal/service/billing_service_test.go b/backend/internal/service/billing_service_test.go index 5320df9f1..8f322b54e 100644 --- a/backend/internal/service/billing_service_test.go +++ b/backend/internal/service/billing_service_test.go @@ -158,17 +158,19 @@ func TestGetModelPricing_FallbackWarnPerModelNotGlobal(t *testing.T) { require.Equal(t, 0, strings.Count(out, "model: GLM-5.2"), out) // 大写经 ToLower 归一,不应单独成行 } -// 回归:glm-5.2 仍解析到 glm-5 兜底价(计费金额不变,防止日志改动掩盖未来计费回归)。 -func TestGetModelPricing_GLM52FallsBackToGLM5Price(t *testing.T) { +// 回归:glm-5.2 必须命中自己的兜底价,不能被 strings.Contains("glm-5") 抢成 glm-5 价。 +// 历史 bug:兜底表缺 glm-5.2 条目,使用记录按 $1.00/$3.20 计费,比官方 $1.40/$4.40 少收约 27%。 +func TestGetModelPricing_GLM52UsesOwnPrice(t *testing.T) { svc := newTestBillingService() got, err := svc.GetModelPricing("glm-5.2") require.NoError(t, err) require.NotNil(t, got) - // glm-5 base:Input 1e-6 / Output 3.2e-6(见 TestGetFallbackPricing_FamilyMatching)。 - require.InDelta(t, 1e-6, got.InputPricePerToken, 1e-12) - require.InDelta(t, 3.2e-6, got.OutputPricePerToken, 1e-12) + // 官方 z.ai 口径:与 glm-5.1 同价(见 TestGetFallbackPricing_FamilyMatching)。 + require.InDelta(t, 1.4e-6, got.InputPricePerToken, 1e-12) + require.InDelta(t, 4.4e-6, got.OutputPricePerToken, 1e-12) + require.InDelta(t, 0.26e-6, got.CacheReadPricePerToken, 1e-12) } func TestGetModelPricing_UnknownClaudeModelFallsBackToSonnet(t *testing.T) { @@ -489,6 +491,13 @@ func TestGetFallbackPricing_FamilyMatching(t *testing.T) { }, // ---- 智谱 GLM(z.ai USD 口径)---- + { + name: "glm 5.2 flagship", + model: "glm-5.2", + expectedInput: 1.4e-6, + expectedOutput: floatPtr(4.4e-6), + expectedCacheRead: floatPtr(0.26e-6), + }, { name: "glm 5.1 flagship", model: "glm-5.1", @@ -572,7 +581,7 @@ func TestGetFallbackPricing_FamilyMatching(t *testing.T) { expectedInput: 0.1e-6, expectedOutput: floatPtr(0.1e-6), }, - // 关键:5.1 必须先于 5 匹配(避免被 glm-5 抢走) + // 关键:5.1 / 5.2 必须先于 5 匹配(避免被 glm-5 抢走) { name: "glm 5.1 vs glm 5 ordering (verbatim 5.1)", model: "glm-5.1", @@ -580,6 +589,13 @@ func TestGetFallbackPricing_FamilyMatching(t *testing.T) { expectedOutput: floatPtr(4.4e-6), expectedCacheRead: floatPtr(0.26e-6), }, + { + name: "glm 5.2 vs glm 5 ordering (verbatim 5.2)", + model: "glm-5.2", + expectedInput: 1.4e-6, // = glm-5.2 价格(不是 glm-5 的 1e-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",