diff --git a/backend/internal/handler/gateway_handler.go b/backend/internal/handler/gateway_handler.go index ecd7bc7e38..8e1bbd2ae0 100644 --- a/backend/internal/handler/gateway_handler.go +++ b/backend/internal/handler/gateway_handler.go @@ -1440,7 +1440,7 @@ func modelListingSource(platform string, availableModels, fallbackModels []strin func defaultCodexModelIDsForPlatform(platform string) []string { switch platform { case service.PlatformDeepseek: - return []string{"deepseek-v4-pro", "deepseek-v4-flash"} + return []string{"deepseek-v4-pro", "deepseek-v4-flash", "deepseek-flash"} case service.PlatformMiniMax: return []string{"MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.5"} default: diff --git a/backend/internal/handler/gateway_models_test.go b/backend/internal/handler/gateway_models_test.go index 9252851cd6..26c0c72ec4 100644 --- a/backend/internal/handler/gateway_models_test.go +++ b/backend/internal/handler/gateway_models_test.go @@ -914,7 +914,7 @@ func TestDefaultModelIDsForPlatform_CNProvidersKeepClaudeDefaults(t *testing.T) } func TestDefaultCodexModelIDsForPlatform_DeepSeekUsesDeepSeekModels(t *testing.T) { - require.Equal(t, []string{"deepseek-v4-pro", "deepseek-v4-flash"}, defaultCodexModelIDsForPlatform(service.PlatformDeepseek)) + require.Equal(t, []string{"deepseek-v4-pro", "deepseek-v4-flash", "deepseek-flash"}, defaultCodexModelIDsForPlatform(service.PlatformDeepseek)) require.Equal(t, []string{"MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.5"}, defaultCodexModelIDsForPlatform(service.PlatformMiniMax)) require.Equal(t, defaultModelIDsForPlatform(service.PlatformAnthropic), defaultCodexModelIDsForPlatform(service.PlatformAnthropic)) } diff --git a/backend/internal/service/account_stats_pricing_test.go b/backend/internal/service/account_stats_pricing_test.go index 7d4d8c7d03..e53810b279 100644 --- a/backend/internal/service/account_stats_pricing_test.go +++ b/backend/internal/service/account_stats_pricing_test.go @@ -659,7 +659,7 @@ func TestTryModelFilePricing_DeepSeekPeakPricing(t *testing.T) { name string input, output, cacheReadPrice float64 }{ - {"deepseek-v4-flash", 2.2e-7, 6.6e-7, 7e-9}, + {"deepseek-v4-flash", 1.5e-7, 6e-7, 3e-9}, {"deepseek-v4-pro", 6.6e-7, 1.98e-6, 2.2e-8}, } { for _, usage := range []struct { @@ -711,7 +711,7 @@ func TestResolveAccountStatsCost_DeepSeekPricingPriority(t *testing.T) { noChannel bool want float64 }{ - {name: "catalog", want: 1000 * 2.2e-7 * 2}, + {name: "catalog", want: 1000 * 1.5e-7 * 2}, {name: "custom_rule", customRule: true, want: 1}, {name: "custom_rule_before_customer_price", customRule: true, applyPricing: true, want: 1}, {name: "customer_price", applyPricing: true, want: 0.75}, diff --git a/backend/internal/service/billing_context_schedule.go b/backend/internal/service/billing_context_schedule.go index d1f994707c..0be6d7a07a 100644 --- a/backend/internal/service/billing_context_schedule.go +++ b/backend/internal/service/billing_context_schedule.go @@ -7,6 +7,8 @@ import ( "sort" "strconv" "time" + + "github.com/Wei-Shaw/sub2api/internal/pkg/timezone" ) // ContextPricingBasis 阶梯的计价基准。当前只有整单口径;历史上的 @@ -222,7 +224,10 @@ func (s *BillingService) contextPricingBreakpoints(resolver *ModelPricingResolve if pricing == nil { return plan } - pricing = s.applyModelSpecificPricingPolicy(model, pricing) + // 该路径无既有计费时点(ContextPricingScheduleInput 无时间字段),显式传 + // 当前时刻;此处 pricing 仅取 LongContextInputThreshold 等时间无关字段, + // DeepSeek pro→Flash 切换不影响断点结果。 + pricing = s.applyModelSpecificPricingPolicyEx(model, pricing, true, timezone.Now()) if pricing.LongContextInputThreshold <= 0 { return plan } diff --git a/backend/internal/service/billing_service.go b/backend/internal/service/billing_service.go index f09f606eb7..76c1adfc43 100644 --- a/backend/internal/service/billing_service.go +++ b/backend/internal/service/billing_service.go @@ -262,14 +262,21 @@ func resolvedChannelTimeMultiplier(resolved *ResolvedPricing, at time.Time) floa // sources can price the requested model. var ErrModelPricingUnavailable = errors.New("pricing not found") -// ---- DeepSeek 官方低谷价($/token,2026-08-23 起生效)---- -// Source: https://api-docs.deepseek.com/quick_start/pricing +// ---- DeepSeek 官方低谷价($/token)---- +// 2026-09-10 官方公告:DeepSeek-V4.1-Flash(新名 deepseek-flash)大幅降价, +// Flash 低谷价降为 $0.15/$0.60/$0.003 per MTok(输入缓存未命中/输出/缓存命中); +// deepseek-v4-pro 名义价格暂不变,但自北京时间 2026-09-14 12:00(04:00 UTC)起 +// 其请求被上游路由到 V4.1-Flash 并按 Flash 价计费(见 deepseekProBilledAsFlash)。 +// Source: https://api-docs.deepseek.com/news/news260910 +// +// https://api-docs.deepseek.com/quick_start/pricing +// // 高峰价 = 2× 低谷价;高峰时段 01:00–04:00 与 06:00–10:00 UTC(仅工作日), // 北京时间周六/周日全天低谷。时段判定见 deepseekPeakMultiplierAt。 const ( - deepseekFlashOffPeakInputPrice = 2.2e-7 // $0.22 per MTok (cache miss) - deepseekFlashOffPeakOutputPrice = 6.6e-7 // $0.66 per MTok - deepseekFlashOffPeakCacheRead = 7e-9 // $0.007 per MTok (cache hit) + deepseekFlashOffPeakInputPrice = 1.5e-7 // $0.15 per MTok (cache miss) + deepseekFlashOffPeakOutputPrice = 6.0e-7 // $0.60 per MTok + deepseekFlashOffPeakCacheRead = 3e-9 // $0.003 per MTok (cache hit) deepseekProOffPeakInputPrice = 6.6e-7 // $0.66 per MTok (cache miss) deepseekProOffPeakOutputPrice = 1.98e-6 // $1.98 per MTok deepseekProOffPeakCacheRead = 2.2e-8 // $0.022 per MTok (cache hit) @@ -302,6 +309,27 @@ func deepseekPeakMultiplierAt(now time.Time) float64 { return 1.0 } +// deepseekProRoutesToFlashAt:官方公告自北京时间 2026-09-14 12:00(04:00 UTC)起, +// 所有 deepseek-v4-pro 请求被上游路由到 V4.1-Flash 并按 Flash 价计费(直至未来 +// V4.1 Pro 上线)。Source: https://api-docs.deepseek.com/news/news260910 +var deepseekProRoutesToFlashAt = time.Date(2026, 9, 14, 4, 0, 0, 0, time.UTC) + +// deepseekProBilledAsFlash 报告指定计费时点 deepseek-v4-pro 是否已按 Flash 价 +// 计费:计费时点到达或晚于切换时点返回 true;零值时点回退当前时刻(与峰谷 +// 倍率的取时点方式一致,见 calculateTokenCost)。 +func deepseekProBilledAsFlash(pricingAt time.Time) bool { + if pricingAt.IsZero() { + pricingAt = timezone.Now() + } + return !pricingAt.Before(deepseekProRoutesToFlashAt) +} + +// isDeepSeekProModel 判断模型名是否归入 deepseek-v4-pro 档(含版本化名称, +// 如 deepseek-v4-pro-0813)。 +func isDeepSeekProModel(model string) bool { + return strings.Contains(strings.ToLower(strings.TrimSpace(model)), "deepseek-v4-pro") +} + // BillingService 计费服务 type BillingService struct { cfg *config.Config @@ -557,10 +585,11 @@ func (s *BillingService) initFallbackPricing() { // ---- DeepSeek 系列 ---- // Source: https://api-docs.deepseek.com/quick_start/pricing - // 官方口径(2026-08-23 起生效):现行模型为 deepseek-v4-flash / - // deepseek-v4-pro / deepseek-v4-flash-vision-exp;deepseek-chat / - // deepseek-reasoner 已停止服务,其余 deepseek-*(含未知型号)统一按 - // flash 价兜底(见 getFallbackPricing),避免计费中断。 + // 官方口径(2026-09-10 公告降价后):现行模型为 deepseek-flash(= + // DeepSeek-V4.1-Flash,旧名 deepseek-v4-flash 兼容路由)/ deepseek-v4-pro / + // deepseek-v4-flash-vision-exp;deepseek-chat / deepseek-reasoner 已停止服务, + // 其余 deepseek-*(含未知型号)统一按 flash 价兜底(见 getFallbackPricing), + // 避免计费中断。 // 以下均为官方低谷价;高峰价 = 2× 低谷价(高峰时段 01:00–04:00 // 与 06:00–10:00 UTC,仅工作日;北京时间周六/周日全天低谷),见 deepseekPeakMultiplierAt。 s.fallbackPrices["deepseek-v4-pro"] = &ModelPricing{ @@ -570,9 +599,9 @@ func (s *BillingService) initFallbackPricing() { SupportsCacheBreakdown: false, } s.fallbackPrices["deepseek-v4-flash"] = &ModelPricing{ - InputPricePerToken: deepseekFlashOffPeakInputPrice, // $0.22 per MTok (cache miss, off-peak) - OutputPricePerToken: deepseekFlashOffPeakOutputPrice, // $0.66 per MTok - CacheReadPricePerToken: deepseekFlashOffPeakCacheRead, // $0.007 per MTok (cache hit) + InputPricePerToken: deepseekFlashOffPeakInputPrice, // $0.15 per MTok (cache miss, off-peak) + OutputPricePerToken: deepseekFlashOffPeakOutputPrice, // $0.60 per MTok + CacheReadPricePerToken: deepseekFlashOffPeakCacheRead, // $0.003 per MTok (cache hit) SupportsCacheBreakdown: false, } s.fallbackPrices["deepseek-v4-flash-vision-exp"] = &ModelPricing{ @@ -1167,6 +1196,14 @@ func (s *BillingService) HasIdentifiedTokenPricing(model string) bool { // GetModelPricing 获取模型价格配置 func (s *BillingService) GetModelPricing(model string) (*ModelPricing, error) { + // 无显式计费时点,DeepSeek pro→Flash 切换按当前时刻判定。 + return s.getModelPricingAt(model, timezone.Now()) +} + +// getModelPricingAt 是 GetModelPricing 的带计费时点内部变体:pricingAt 显式 +// 驱动 DeepSeek pro→Flash 切换判定(切换点前 Pro 价、之后 Flash 价),使 +// 展示/估算路径可与历史补账同刻复算,测试也能用固定时点钉住断言。 +func (s *BillingService) getModelPricingAt(model string, pricingAt time.Time) (*ModelPricing, error) { // 标准化模型名称(转小写) model = strings.ToLower(model) @@ -1188,7 +1225,7 @@ func (s *BillingService) GetModelPricing(model string) (*ModelPricing, error) { price5m := litellmPricing.CacheCreationInputTokenCost price1h := litellmPricing.CacheCreationInputTokenCostAbove1hr enableBreakdown := price1h > 0 && price1h > price5m - return s.applyModelSpecificPricingPolicy(model, &ModelPricing{ + return s.applyModelSpecificPricingPolicyEx(model, &ModelPricing{ InputPricePerToken: litellmPricing.InputCostPerToken, InputPricePerTokenPriority: litellmPricing.InputCostPerTokenPriority, OutputPricePerToken: litellmPricing.OutputCostPerToken, @@ -1207,7 +1244,7 @@ func (s *BillingService) GetModelPricing(model string) (*ModelPricing, error) { LongContextOutputMultiplier: litellmPricing.LongContextOutputCostMultiplier, ImageInputPricePerToken: litellmPricing.InputCostPerImageToken, ImageOutputPricePerToken: litellmPricing.OutputCostPerImageToken, - }), nil + }, true, pricingAt), nil } } @@ -1219,7 +1256,7 @@ func (s *BillingService) GetModelPricing(model string) (*ModelPricing, error) { if _, seen := s.fallbackWarnSeen.LoadOrStore(model, struct{}{}); !seen { log.Printf("[Billing] Using fallback pricing for model: %s", model) } - return s.applyModelSpecificPricingPolicy(model, fallback), nil + return s.applyModelSpecificPricingPolicyEx(model, fallback, true, pricingAt), nil } return nil, fmt.Errorf("%w for model: %s", ErrModelPricingUnavailable, model) @@ -1398,20 +1435,22 @@ func (s *BillingService) calculateTokenCost(resolved *ResolvedPricing, input Cos return nil, fmt.Errorf("no pricing available for model: %s: %w", input.Model, ErrModelPricingUnavailable) } + // 计费时点:优先请求级 PricingAt(历史补账与 DeepSeek pro→Flash 切换判定 + // 同源),零值回退当前时刻。 + pricingAt := input.PricingAt + if pricingAt.IsZero() { + pricingAt = timezone.Now() + } + // 默认价卡(Source=LiteLLM)应用 DeepSeek 官方价强制覆盖(幂等,GetModelPricing // 内部已强制过);分组/渠道自定义定价保留运营者配置,不强制覆盖官方价。 - pricing = s.applyModelSpecificPricingPolicyEx(input.Model, pricing, resolved.Source == PricingSourceLiteLLM) + pricing = s.applyModelSpecificPricingPolicyEx(input.Model, pricing, resolved.Source == PricingSourceLiteLLM, pricingAt) // DeepSeek 模型默认价卡按官方峰谷口径调整:高峰时段(01:00–04:00 与 // 06:00–10:00 UTC,仅工作日;北京时间周末全天低谷)按 2× 低谷价计费。 // 仅作用于默认价卡(Source=LiteLLM,无分组/渠道自定义定价)——分组/渠道 - // 自定义定价保持运营者语义,不叠加。PricingAt 为零值时回退当前时刻。 - // 先克隆再乘,避免污染共享 fallbackPrices 指针。 + // 自定义定价保持运营者语义,不叠加。先克隆再乘,避免污染共享 fallbackPrices 指针。 if resolved.Source == PricingSourceLiteLLM && isDeepSeekModel(input.Model) { - pricingAt := input.PricingAt - if pricingAt.IsZero() { - pricingAt = timezone.Now() - } if mult := deepseekPeakMultiplierAt(pricingAt); mult > 1 { cloned := *pricing cloned.InputPricePerToken *= mult @@ -1670,36 +1709,43 @@ func (s *BillingService) calculateCostInternalWithPolicy( // 强制覆盖;GPT-5.6 缺 cache_write 价时按官方规则补 1.25 倍输入价;Fast/priority // 档按业务倍率改写(本地/远程目录的 priority 价可能沿用官方旧口径)。长上下文 // 阶梯不在此处补齐:一律由目录数据(above_XXXk 折算或显式 long_context_* 字段) -// 驱动。默认强制 DeepSeek 官方价——该路径仅被默认价卡(GetModelPricing 内部) -// 调用;分组/渠道自定义定价路径用带参数的 applyModelSpecificPricingPolicyEx -// 关闭强制,保留运营者配置。 +// 驱动。强制 DeepSeek 官方价且无显式计费时点(pro→Flash 切换按当前时刻判定), +// 供无既有时点的策略修正场景与测试使用;计费/展示主路径分别经 +// calculateTokenCost 与 getModelPricingAt 显式传时点,分组/渠道自定义定价 +// 用 applyModelSpecificPricingPolicyEx 关闭强制,保留运营者配置。 func (s *BillingService) applyModelSpecificPricingPolicy(model string, pricing *ModelPricing) *ModelPricing { - return s.applyModelSpecificPricingPolicyEx(model, pricing, true) + return s.applyModelSpecificPricingPolicyEx(model, pricing, true, time.Time{}) } // applyModelSpecificPricingPolicyEx 与 applyModelSpecificPricingPolicy 相同, -// 但由调用方控制是否强制 DeepSeek 官方价(forceDeepSeekRates)。 +// 但由调用方控制是否强制 DeepSeek 官方价(forceDeepSeekRates),并显式传入 +// 计费时点 pricingAt(零值表示按当前时刻判定)。 // calculateTokenCost 对分组/渠道自定义定价(Source 非 LiteLLM)传 false: // 强制覆盖会把运营者配置的售价盖回官方价,违反自定义定价语义。 -func (s *BillingService) applyModelSpecificPricingPolicyEx(model string, pricing *ModelPricing, forceDeepSeekRates bool) *ModelPricing { +func (s *BillingService) applyModelSpecificPricingPolicyEx(model string, pricing *ModelPricing, forceDeepSeekRates bool, pricingAt time.Time) *ModelPricing { if pricing == nil { return nil } // DeepSeek 模型:无论 JSON/远端价格表给什么价,一律强制官方低谷价 - // (2026-08-23 起生效)。这是覆盖远端旧价的关键——远端仓库不可改,生产会先 - // 拉到旧价,必须在此兜底修正;克隆后再覆盖,避免污染共享 fallbackPrices 指针。 + // (Flash 三档为 2026-09-10 官方降价后口径)。这是覆盖远端旧价的关键——远端 + // 仓库不可改,生产会先拉到旧价,必须在此兜底修正;克隆后再覆盖,避免污染 + // 共享 fallbackPrices 指针。 // 档位判定:含 "deepseek-v4-pro" 的版本化名称(如 deepseek-v4-pro-0813)归 pro 档, // 其余 deepseek-*(含已停服的 chat/reasoner 与未知型号)统一归 flash 档。 + // 2026-09-14 04:00 UTC 起上游把 pro 请求路由到 V4.1-Flash,pro 档改按 + // Flash 三档价计费;历史时点(早于切换时刻)仍按 Pro 价。 // 高峰时段倍率不在本函数处理,由 calculateTokenCost 按 deepseekPeakMultiplierAt // 对默认价卡另行叠加(分组/渠道自定义定价不叠加)。 if forceDeepSeekRates && isDeepSeekModel(model) { cloned := *pricing - if strings.Contains(strings.ToLower(strings.TrimSpace(model)), "deepseek-v4-pro") { + if isDeepSeekProModel(model) && !deepseekProBilledAsFlash(pricingAt) { cloned.InputPricePerToken = deepseekProOffPeakInputPrice cloned.OutputPricePerToken = deepseekProOffPeakOutputPrice cloned.CacheReadPricePerToken = deepseekProOffPeakCacheRead } else { - // deepseek-v4-flash / deepseek-v4-flash-vision-exp 与其余 deepseek-* 共用 flash 价。 + // deepseek-flash(= V4.1-Flash)、deepseek-v4-flash / + // deepseek-v4-flash-vision-exp 与其余 deepseek-* 共用 flash 价; + // 切换时点之后的 pro 请求同样按 flash 价计费。 cloned.InputPricePerToken = deepseekFlashOffPeakInputPrice cloned.OutputPricePerToken = deepseekFlashOffPeakOutputPrice cloned.CacheReadPricePerToken = deepseekFlashOffPeakCacheRead diff --git a/backend/internal/service/billing_service_test.go b/backend/internal/service/billing_service_test.go index 811419e861..0a68e07ad7 100644 --- a/backend/internal/service/billing_service_test.go +++ b/backend/internal/service/billing_service_test.go @@ -514,38 +514,46 @@ func TestGetFallbackPricing_FamilyMatching(t *testing.T) { { name: "deepseek v4 flash", model: "deepseek-v4-flash", - expectedInput: 2.2e-7, - expectedOutput: floatPtr(6.6e-7), - expectedCacheRead: floatPtr(7e-9), + expectedInput: 1.5e-7, + expectedOutput: floatPtr(6e-7), + expectedCacheRead: floatPtr(3e-9), + }, + { + // deepseek-flash(= V4.1-Flash 新名)经前缀兜底同样命中 flash 价卡。 + name: "deepseek flash v41 name maps to flash", + model: "deepseek-flash", + expectedInput: 1.5e-7, + expectedOutput: floatPtr(6e-7), + expectedCacheRead: floatPtr(3e-9), }, { name: "deepseek v4 flash vision exp", model: "deepseek-v4-flash-vision-exp", - expectedInput: 2.2e-7, - expectedOutput: floatPtr(6.6e-7), - expectedCacheRead: floatPtr(7e-9), + expectedInput: 1.5e-7, + expectedOutput: floatPtr(6e-7), + expectedCacheRead: floatPtr(3e-9), }, { // deepseek-chat / deepseek-reasoner 已停止服务,统一按 flash 价兜底。 name: "deepseek chat discontinued maps to flash", model: "deepseek-chat", - expectedInput: 2.2e-7, - expectedOutput: floatPtr(6.6e-7), - expectedCacheRead: floatPtr(7e-9), + expectedInput: 1.5e-7, + expectedOutput: floatPtr(6e-7), + expectedCacheRead: floatPtr(3e-9), }, { name: "deepseek reasoner discontinued maps to flash", model: "deepseek-reasoner", - expectedInput: 2.2e-7, - expectedOutput: floatPtr(6.6e-7), - expectedCacheRead: floatPtr(7e-9), + expectedInput: 1.5e-7, + expectedOutput: floatPtr(6e-7), + expectedCacheRead: floatPtr(3e-9), }, { name: "unknown deepseek maps to flash", model: "deepseek-foo", - expectedInput: 2.2e-7, - expectedOutput: floatPtr(6.6e-7), - expectedCacheRead: floatPtr(7e-9), + expectedInput: 1.5e-7, + expectedOutput: floatPtr(6e-7), + expectedCacheRead: floatPtr(3e-9), }, // ---- 智谱 GLM(z.ai USD 口径)---- diff --git a/backend/internal/service/deepseek_pricing_test.go b/backend/internal/service/deepseek_pricing_test.go index 3b813929a4..7862d82e0e 100644 --- a/backend/internal/service/deepseek_pricing_test.go +++ b/backend/internal/service/deepseek_pricing_test.go @@ -58,7 +58,7 @@ func TestDeepseekPeakMultiplierAt(t *testing.T) { func TestIsDeepSeekModel(t *testing.T) { deepseek := []string{ - "deepseek-v4-flash", "deepseek-v4-pro", "deepseek-v4-flash-vision-exp", + "deepseek-flash", "deepseek-v4-flash", "deepseek-v4-pro", "deepseek-v4-flash-vision-exp", "deepseek-chat", "deepseek-reasoner", "deepseek-v3-2-251201", "deepseek-coder", "deepseek-foo", "deepseek-v4-pro-0813", "DEEPSEEK-V4-PRO", " deepseek-v4-flash ", @@ -85,8 +85,8 @@ func TestCalculateCostUnified_DeepseekDefaultCardPeakMultiplier(t *testing.T) { resolver := NewModelPricingResolver(nil, bs) tokens := UsageTokens{InputTokens: 1000, OutputTokens: 500, CacheReadTokens: 1000} - // 低谷成本:1000*2.2e-7 + 500*6.6e-7 + 1000*7e-9 = 5.57e-4 - offPeakTotal := 1000*2.2e-7 + 500*6.6e-7 + 1000*7e-9 + // 低谷成本(2026-09-10 官方新价):1000*1.5e-7 + 500*6e-7 + 1000*3e-9 = 4.53e-4 + offPeakTotal := 1000*1.5e-7 + 500*6e-7 + 1000*3e-9 offPeak, err := bs.CalculateCostUnified(CostInput{ Ctx: context.Background(), Model: "deepseek-v4-flash", Tokens: tokens, @@ -134,7 +134,7 @@ func TestCalculateCostUnified_DeepseekVersionedNamePeakMultiplier(t *testing.T) resolver := NewModelPricingResolver(nil, bs) tokens := UsageTokens{InputTokens: 1000, OutputTokens: 500, CacheReadTokens: 1000} - offPeakTotal := 1000*2.2e-7 + 500*6.6e-7 + 1000*7e-9 + offPeakTotal := 1000*1.5e-7 + 500*6e-7 + 1000*3e-9 offPeak, err := bs.CalculateCostUnified(CostInput{ Ctx: context.Background(), Model: "deepseek-v4-flash-0731", Tokens: tokens, @@ -170,8 +170,8 @@ func TestCalculateCostUnified_DeepseekGroupPricingNotScaledByPeak(t *testing.T) require.Equal(t, PricingSourceGroup, resolved.Source) tokens := UsageTokens{InputTokens: 1000, OutputTokens: 500, CacheReadTokens: 1000} - // 分组自定义价:1000*1e-6 + 500*2e-6 + 1000*7e-9(缓存读沿用官方 flash 价) - groupTotal := 1000*1e-6 + 500*2e-6 + 1000*7e-9 + // 分组自定义价:1000*1e-6 + 500*2e-6 + 1000*3e-9(缓存读沿用官方 flash 价) + groupTotal := 1000*1e-6 + 500*2e-6 + 1000*3e-9 for _, pricingAt := range []time.Time{ time.Date(2026, 8, 24, 12, 0, 0, 0, time.UTC), // 低谷 @@ -238,6 +238,7 @@ func TestCalculateCostUnified_DeepseekPricingAtZeroFallsBackToNow(t *testing.T) func TestGetModelPricing_DeepseekForcesOfficialRatesOverJSON(t *testing.T) { // JSON 给任意价(模拟远端旧价/占位价),deepseek-* 必须被强制覆盖为官方低谷价。 pricingSvc := &PricingService{pricingData: map[string]*LiteLLMModelPricing{ + "deepseek-flash": {InputCostPerToken: 1e-6, OutputCostPerToken: 2e-6, CacheReadInputTokenCost: 1e-8}, "deepseek-v4-flash": {InputCostPerToken: 1e-6, OutputCostPerToken: 2e-6, CacheReadInputTokenCost: 1e-8}, "deepseek-v4-pro": {InputCostPerToken: 1e-6, OutputCostPerToken: 2e-6, CacheReadInputTokenCost: 1e-8}, "deepseek-v4-flash-vision-exp": {InputCostPerToken: 1e-6, OutputCostPerToken: 2e-6, CacheReadInputTokenCost: 1e-8}, @@ -250,31 +251,64 @@ func TestGetModelPricing_DeepseekForcesOfficialRatesOverJSON(t *testing.T) { model string input, output, cacheRead float64 }{ - {"deepseek-v4-flash", 2.2e-7, 6.6e-7, 7e-9}, - {"deepseek-v4-flash-vision-exp", 2.2e-7, 6.6e-7, 7e-9}, - {"deepseek-v4-pro", 6.6e-7, 1.98e-6, 2.2e-8}, + // 2026-09-10 官方降价后:deepseek-flash(V4.1-Flash 新名)与旧名 + // deepseek-v4-flash 同按 Flash 新价。 + {"deepseek-flash", 1.5e-7, 6e-7, 3e-9}, + {"deepseek-v4-flash", 1.5e-7, 6e-7, 3e-9}, + {"deepseek-v4-flash-vision-exp", 1.5e-7, 6e-7, 3e-9}, // 已停服的 chat/reasoner:即使 JSON 有旧条目也按 flash 价兜底。 - {"deepseek-chat", 2.2e-7, 6.6e-7, 7e-9}, - {"deepseek-reasoner", 2.2e-7, 6.6e-7, 7e-9}, + {"deepseek-chat", 1.5e-7, 6e-7, 3e-9}, + {"deepseek-reasoner", 1.5e-7, 6e-7, 3e-9}, } for _, tt := range tests { t.Run(tt.model, func(t *testing.T) { + // flash 档三档价与 pro→Flash 切换无关,GetModelPricing 断言不随时间翻转。 pricing, err := bs.GetModelPricing(tt.model) require.NoError(t, err) require.InDelta(t, tt.input, pricing.InputPricePerToken, 1e-15) require.InDelta(t, tt.output, pricing.OutputPricePerToken, 1e-15) require.InDelta(t, tt.cacheRead, pricing.CacheReadPricePerToken, 1e-15) + // 固定时点(切换点之后的 2026-10-01)复核:仍走 Flash 新价。 + atPricing, err := bs.getModelPricingAt(tt.model, time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC)) + require.NoError(t, err) + require.InDelta(t, tt.input, atPricing.InputPricePerToken, 1e-15) + require.InDelta(t, tt.output, atPricing.OutputPricePerToken, 1e-15) + require.InDelta(t, tt.cacheRead, atPricing.CacheReadPricePerToken, 1e-15) require.True(t, bs.HasIdentifiedTokenPricing(tt.model)) }) } + // pro 档(含版本化名称):断言经固定时点的 getModelPricingAt,不依赖墙上时钟。 + // 2026-08-01 早于切换点 2026-09-14 04:00 UTC → Pro 价。 + for _, model := range []string{"deepseek-v4-pro", "deepseek-v4-pro-0813"} { + t.Run(model+"/before-cutoff", func(t *testing.T) { + pricing, err := bs.getModelPricingAt(model, time.Date(2026, 8, 1, 0, 0, 0, 0, time.UTC)) + require.NoError(t, err) + require.InDelta(t, 6.6e-7, pricing.InputPricePerToken, 1e-15) + require.InDelta(t, 1.98e-6, pricing.OutputPricePerToken, 1e-15) + require.InDelta(t, 2.2e-8, pricing.CacheReadPricePerToken, 1e-15) + }) + } + + // 半开边界钉死:2026-09-14 03:59:59 仍 Pro 价,04:00:00 整起 Flash 新价。 + proBefore, err := bs.getModelPricingAt("deepseek-v4-pro", time.Date(2026, 9, 14, 3, 59, 59, 0, time.UTC)) + require.NoError(t, err) + require.InDelta(t, 6.6e-7, proBefore.InputPricePerToken, 1e-15) + require.InDelta(t, 1.98e-6, proBefore.OutputPricePerToken, 1e-15) + require.InDelta(t, 2.2e-8, proBefore.CacheReadPricePerToken, 1e-15) + proAtCutoff, err := bs.getModelPricingAt("deepseek-v4-pro", time.Date(2026, 9, 14, 4, 0, 0, 0, time.UTC)) + require.NoError(t, err) + require.InDelta(t, 1.5e-7, proAtCutoff.InputPricePerToken, 1e-15) + require.InDelta(t, 6e-7, proAtCutoff.OutputPricePerToken, 1e-15) + require.InDelta(t, 3e-9, proAtCutoff.CacheReadPricePerToken, 1e-15) + // 版本化名称(不在 JSON / fallbackPrices 精确表中):按子串归档计价。 + // flash-0731 归 flash 档,三档价与切换无关,GetModelPricing 断言稳定。 versioned := []struct { model string input, output, cacheRead float64 }{ - {"deepseek-v4-pro-0813", 6.6e-7, 1.98e-6, 2.2e-8}, - {"deepseek-v4-flash-0731", 2.2e-7, 6.6e-7, 7e-9}, + {"deepseek-v4-flash-0731", 1.5e-7, 6e-7, 3e-9}, } for _, tt := range versioned { t.Run(tt.model, func(t *testing.T) { @@ -289,7 +323,7 @@ func TestGetModelPricing_DeepseekForcesOfficialRatesOverJSON(t *testing.T) { func TestGetModelPricing_UnknownDeepseekMapsToFlash(t *testing.T) { // JSON 含 $0 占位条目(如旧 deepseek-v3-2-251201):未知 deepseek-* 不再 - // fail-closed,统一按 flash 价兜底(2.2e-7/6.6e-7/7e-9),不得按 $0 计费。 + // fail-closed,统一按 flash 价兜底(1.5e-7/6e-7/3e-9),不得按 $0 计费。 pricingSvc := &PricingService{pricingData: map[string]*LiteLLMModelPricing{ "deepseek-v3-2-251201": {InputCostPerToken: 0, OutputCostPerToken: 0}, }} @@ -299,9 +333,9 @@ func TestGetModelPricing_UnknownDeepseekMapsToFlash(t *testing.T) { t.Run(m, func(t *testing.T) { pricing, err := bs.GetModelPricing(m) require.NoError(t, err) - require.InDelta(t, 2.2e-7, pricing.InputPricePerToken, 1e-15) - require.InDelta(t, 6.6e-7, pricing.OutputPricePerToken, 1e-15) - require.InDelta(t, 7e-9, pricing.CacheReadPricePerToken, 1e-15) + require.InDelta(t, 1.5e-7, pricing.InputPricePerToken, 1e-15) + require.InDelta(t, 6e-7, pricing.OutputPricePerToken, 1e-15) + require.InDelta(t, 3e-9, pricing.CacheReadPricePerToken, 1e-15) }) } } @@ -329,8 +363,9 @@ func TestDeepseekPricingFileMatchesOfficialRates(t *testing.T) { model string input, output, cacheRead float64 }{ - {"deepseek-v4-flash", 2.2e-7, 6.6e-7, 7e-9}, - {"deepseek-v4-flash-vision-exp", 2.2e-7, 6.6e-7, 7e-9}, + {"deepseek-flash", 1.5e-7, 6e-7, 3e-9}, + {"deepseek-v4-flash", 1.5e-7, 6e-7, 3e-9}, + {"deepseek-v4-flash-vision-exp", 1.5e-7, 6e-7, 3e-9}, {"deepseek-v4-pro", 6.6e-7, 1.98e-6, 2.2e-8}, } for _, tt := range tests { @@ -343,3 +378,68 @@ func TestDeepseekPricingFileMatchesOfficialRates(t *testing.T) { }) } } + +// --------------------------------------------------------------------------- +// 2026-09-10 官方降价:deepseek-flash(V4.1-Flash 新名)与旧名同价; +// 2026-09-14 04:00 UTC 起 deepseek-v4-pro 按上游路由改按 Flash 价计费 +// --------------------------------------------------------------------------- + +func TestCalculateCostUnified_DeepseekFlashAndLegacyFlashShareNewRates(t *testing.T) { + bs := newTestBillingService() + resolver := NewModelPricingResolver(nil, bs) + + tokens := UsageTokens{InputTokens: 1000, OutputTokens: 500, CacheReadTokens: 1000} + // 2026-09-10 官方新低谷价:1000*1.5e-7 + 500*6e-7 + 1000*3e-9 = 4.53e-4 + offPeakTotal := 1000*1.5e-7 + 500*6e-7 + 1000*3e-9 + + // deepseek-flash 与 deepseek-v4-flash 都取 Flash 新价。 + // 时点取切换日 2026-09-14(周一)12:00 UTC 低谷,峰谷倍率不影响断言。 + for _, model := range []string{"deepseek-flash", "deepseek-v4-flash"} { + cost, err := bs.CalculateCostUnified(CostInput{ + Ctx: context.Background(), Model: model, Tokens: tokens, + RateMultiplier: 1.0, Resolver: resolver, + PricingAt: time.Date(2026, 9, 14, 12, 0, 0, 0, time.UTC), + }) + require.NoError(t, err) + require.InDelta(t, offPeakTotal, cost.TotalCost, 1e-10, "model %s must use new flash rates", model) + } +} + +func TestCalculateCostUnified_DeepseekProRoutesToFlashAtCutoff(t *testing.T) { + bs := newTestBillingService() + resolver := NewModelPricingResolver(nil, bs) + + tokens := UsageTokens{InputTokens: 1000, OutputTokens: 500, CacheReadTokens: 1000} + proTotal := 1000*6.6e-7 + 500*1.98e-6 + 1000*2.2e-8 // 切换前 Pro 价 + flashTotal := 1000*1.5e-7 + 500*6e-7 + 1000*3e-9 // 切换后 Flash 价 + + // 切换时点之前(2026-09-13 周日,北京周末全天低谷):仍按 Pro 价。 + before, err := bs.CalculateCostUnified(CostInput{ + Ctx: context.Background(), Model: "deepseek-v4-pro", Tokens: tokens, + RateMultiplier: 1.0, Resolver: resolver, + PricingAt: time.Date(2026, 9, 13, 12, 0, 0, 0, time.UTC), + }) + require.NoError(t, err) + require.InDelta(t, proTotal, before.TotalCost, 1e-10, + "deepseek-v4-pro must use Pro rates before the 2026-09-14 04:00 UTC cutoff") + + // 到达切换时点(2026-09-14 04:00 UTC 整,周一低谷窗口边界外):按 Flash 价。 + after, err := bs.CalculateCostUnified(CostInput{ + Ctx: context.Background(), Model: "deepseek-v4-pro", Tokens: tokens, + RateMultiplier: 1.0, Resolver: resolver, + PricingAt: time.Date(2026, 9, 14, 4, 0, 0, 0, time.UTC), + }) + require.NoError(t, err) + require.InDelta(t, flashTotal, after.TotalCost, 1e-10, + "deepseek-v4-pro must use Flash rates at/after the 2026-09-14 04:00 UTC cutoff") + + // 版本化名称同口径:切换后 deepseek-v4-pro-0813 也按 Flash 价。 + versioned, err := bs.CalculateCostUnified(CostInput{ + Ctx: context.Background(), Model: "deepseek-v4-pro-0813", Tokens: tokens, + RateMultiplier: 1.0, Resolver: resolver, + PricingAt: time.Date(2026, 9, 14, 4, 0, 0, 0, time.UTC), + }) + require.NoError(t, err) + require.InDelta(t, flashTotal, versioned.TotalCost, 1e-10, + "versioned pro names must also route to Flash rates after the cutoff") +} diff --git a/backend/internal/service/gateway_record_usage_test.go b/backend/internal/service/gateway_record_usage_test.go index a0d1c0e180..9dfa556298 100644 --- a/backend/internal/service/gateway_record_usage_test.go +++ b/backend/internal/service/gateway_record_usage_test.go @@ -465,7 +465,7 @@ func TestGatewayServiceRecordUsage_DeepSeekAccountStatsUsesRequestPricingAtAndUp name string offPeakCost float64 }{ - {"deepseek-v4-flash", 1000*2.2e-7 + 500*6.6e-7 + 1000*7e-9}, + {"deepseek-v4-flash", 1000*1.5e-7 + 500*6e-7 + 1000*3e-9}, {"deepseek-v4-pro", 1000*6.6e-7 + 500*1.98e-6 + 1000*2.2e-8}, } { for _, slot := range []struct { diff --git a/backend/internal/service/openai_gateway_record_usage_test.go b/backend/internal/service/openai_gateway_record_usage_test.go index e5dca2d7b7..85b268fbc9 100644 --- a/backend/internal/service/openai_gateway_record_usage_test.go +++ b/backend/internal/service/openai_gateway_record_usage_test.go @@ -575,7 +575,7 @@ func TestOpenAIGatewayServiceRecordUsage_DeepSeekAccountStatsUsesRequestPricingA name string offPeakCost float64 }{ - {"deepseek-v4-flash", 1000*2.2e-7 + 500*6.6e-7 + 1000*7e-9}, + {"deepseek-v4-flash", 1000*1.5e-7 + 500*6e-7 + 1000*3e-9}, {"deepseek-v4-pro", 1000*6.6e-7 + 500*1.98e-6 + 1000*2.2e-8}, } { for _, slot := range []struct { diff --git a/backend/internal/service/openai_gateway_request_body.go b/backend/internal/service/openai_gateway_request_body.go index af4825762b..c476c9726d 100644 --- a/backend/internal/service/openai_gateway_request_body.go +++ b/backend/internal/service/openai_gateway_request_body.go @@ -2180,7 +2180,8 @@ func supportsOpenAIReasoningEffortMax(model string) bool { normalized := strings.ToLower(lastOpenAIModelSegment(model)) normalized = strings.ReplaceAll(normalized, "_", "-") switch { - case strings.HasPrefix(normalized, "deepseek-v4"): + case strings.HasPrefix(normalized, "deepseek-v4"), strings.HasPrefix(normalized, "deepseek-flash"): + // deepseek-flash(= DeepSeek-V4.1-Flash)与 v4 系同为 low/high/max 档位。 return true case strings.HasPrefix(normalized, "glm-"): return true diff --git a/backend/internal/service/openai_gpt56_max_test.go b/backend/internal/service/openai_gpt56_max_test.go index 0b0c8e6fac..fbac20d377 100644 --- a/backend/internal/service/openai_gpt56_max_test.go +++ b/backend/internal/service/openai_gpt56_max_test.go @@ -69,6 +69,7 @@ func TestNormalizeOpenAIReasoningEffortForMaxCapableModels(t *testing.T) { {name: "Terra 保留 max", raw: "max", model: "openai/gpt-5.6-terra", want: "max"}, {name: "Luna 后缀保留 max", raw: "max", model: "gpt-5.6-luna-2026-07-09", want: "max"}, {name: "DeepSeek V4 保留 max", raw: "max", model: "deepseek-v4-pro", want: "max"}, + {name: "DeepSeek Flash 保留 max", raw: "max", model: "deepseek-flash", want: "max"}, {name: "旧 GPT 模型沿用 xhigh", raw: "max", model: "gpt-5.5", want: "xhigh"}, } diff --git a/backend/resources/model-pricing/model_prices_and_context_window.json b/backend/resources/model-pricing/model_prices_and_context_window.json index f971e1c566..579d18db86 100644 --- a/backend/resources/model-pricing/model_prices_and_context_window.json +++ b/backend/resources/model-pricing/model_prices_and_context_window.json @@ -745,15 +745,38 @@ "supports_web_search": true, "supports_xhigh_reasoning_effort": true }, - "deepseek-v4-flash": { - "cache_read_input_token_cost": 7e-09, - "input_cost_per_token": 2.2e-07, + "deepseek-flash": { + "cache_read_input_token_cost": 3e-09, + "input_cost_per_token": 1.5e-07, "litellm_provider": "deepseek", "max_input_tokens": 1048576, "max_output_tokens": 393216, "max_tokens": 393216, "mode": "chat", - "output_cost_per_token": 6.6e-07, + "output_cost_per_token": 6e-07, + "source": "https://api-docs.deepseek.com/quick_start/pricing", + "supported_endpoints": [ + "/v1/chat/completions" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "deepseek-v4-flash": { + "cache_read_input_token_cost": 3e-09, + "input_cost_per_token": 1.5e-07, + "litellm_provider": "deepseek", + "max_input_tokens": 1048576, + "max_output_tokens": 393216, + "max_tokens": 393216, + "mode": "chat", + "output_cost_per_token": 6e-07, "source": "https://api-docs.deepseek.com/quick_start/pricing", "supported_endpoints": [ "/v1/chat/completions" @@ -768,14 +791,14 @@ "supports_tool_choice": true }, "deepseek-v4-flash-vision-exp": { - "cache_read_input_token_cost": 7e-09, - "input_cost_per_token": 2.2e-07, + "cache_read_input_token_cost": 3e-09, + "input_cost_per_token": 1.5e-07, "litellm_provider": "deepseek", "max_input_tokens": 1048576, "max_output_tokens": 393216, "max_tokens": 393216, "mode": "chat", - "output_cost_per_token": 6.6e-07, + "output_cost_per_token": 6e-07, "source": "https://api-docs.deepseek.com/quick_start/pricing", "supported_endpoints": [ "/v1/chat/completions" diff --git a/frontend/src/composables/useModelWhitelist.ts b/frontend/src/composables/useModelWhitelist.ts index d52ede75a4..feed1c37f4 100644 --- a/frontend/src/composables/useModelWhitelist.ts +++ b/frontend/src/composables/useModelWhitelist.ts @@ -116,7 +116,7 @@ const qwenModels = [ // DeepSeek const deepseekModels = [ - 'deepseek-v4-pro', 'deepseek-v4-flash', 'deepseek-v4-flash-vision-exp', + 'deepseek-v4-pro', 'deepseek-v4-flash', 'deepseek-v4-flash-vision-exp', 'deepseek-flash', 'deepseek-coder', 'deepseek-v3', 'deepseek-v3-0324', 'deepseek-r1', 'deepseek-r1-0528',