fix(billing): 适配 DeepSeek V4.1-Flash 新价与 pro→Flash 切换

- flash 低谷价更新为 $0.15/$0.60/$0.003 per MTok(2026-09-10 官方降价)
- deepseek-v4-pro 自 2026-09-14 04:00 UTC 起按 Flash 价计费(上游路由变更)
- 计费时点显式驱动 pro→Flash 切换,定价展示与计费两条路径同源
- deepseek-flash 保留 reasoning_effort=max;补 Codex 模型清单与前端白名单
- 价表 JSON 新增 deepseek-flash 条目并同步 flash 系价格
This commit is contained in:
alfadb
2026-09-10 22:10:29 +08:00
parent 98d86915be
commit ab9bd9e877
13 changed files with 266 additions and 82 deletions
+1 -1
View File
@@ -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:
@@ -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))
}
@@ -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},
@@ -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
}
+78 -32
View File
@@ -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
@@ -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 口径)----
+119 -19
View File
@@ -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")
}
@@ -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 {
@@ -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 {
@@ -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
@@ -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"},
}
@@ -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"
@@ -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',