fix(pricing): align DeepSeek billing with official peak/off-peak rates

同步 DeepSeek 官方峰谷价格与模型定价。
This commit is contained in:
alfadb
2026-08-29 17:30:23 +08:00
committed by GitHub
parent ea4291a92c
commit b5827cfd54
5 changed files with 535 additions and 52 deletions
+124 -12
View File
@@ -11,6 +11,7 @@ import (
"time"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
"github.com/Wei-Shaw/sub2api/internal/pkg/xai"
)
@@ -230,6 +231,46 @@ 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
// 高峰价 = 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)
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)
)
// isDeepSeekModel 判断模型名是否为 DeepSeek 模型(大小写不敏感)。
// 任意 deepseek- 前缀均视为 DeepSeek 模型:官方模型(v4-flash / v4-pro /
// v4-flash-vision-exp)按各自价卡计价,其余 deepseek-*(含已停服的
// deepseek-chat / deepseek-reasoner 与未知型号)统一按 flash 价兜底,
// 避免计费中断;新名字由 fallback warn 日志(每模型每进程一条)暴露,
// 运营者据此更新价卡。
func isDeepSeekModel(model string) bool {
return strings.HasPrefix(strings.ToLower(strings.TrimSpace(model)), "deepseek-")
}
// deepseekPeakMultiplierAt 返回指定时刻的 DeepSeek 官方峰谷定价因子。
// 官方口径(2026-08-23 起生效):高峰价 = 2× 低谷价;高峰时段为
// 01:00–04:00 与 06:00–10:00 UTC(半开区间),仅工作日;
// 周末(北京时间周六/周日)全天低谷。北京时间用固定 +8 偏移(无夏令时)。
func deepseekPeakMultiplierAt(now time.Time) float64 {
beijing := now.In(time.FixedZone("Asia/Shanghai", 8*3600))
switch beijing.Weekday() {
case time.Saturday, time.Sunday:
return 1.0
}
switch h := now.UTC().Hour(); {
case h >= 1 && h < 4, h >= 6 && h < 10:
return 2.0
}
return 1.0
}
// BillingService 计费服务
type BillingService struct {
cfg *config.Config
@@ -466,19 +507,30 @@ func (s *BillingService) initFallbackPricing() {
// 覆盖逻辑见同文件 getFallbackPricing()
// ============================================================
// ---- DeepSeek V4 系列 ----
// ---- DeepSeek 系列 ----
// Source: https://api-docs.deepseek.com/quick_start/pricing
// (deepseek-chat / deepseek-reasoner 为 deepseek-v4-flash 的兼容别名,2026/07/24 弃用)
// 官方口径(2026-08-23 起生效):现行模型为 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{
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)
InputPricePerToken: deepseekProOffPeakInputPrice, // $0.66 per MTok (cache miss, off-peak)
OutputPricePerToken: deepseekProOffPeakOutputPrice, // $1.98 per MTok
CacheReadPricePerToken: deepseekProOffPeakCacheRead, // $0.022 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)
InputPricePerToken: deepseekFlashOffPeakInputPrice, // $0.22 per MTok (cache miss, off-peak)
OutputPricePerToken: deepseekFlashOffPeakOutputPrice, // $0.66 per MTok
CacheReadPricePerToken: deepseekFlashOffPeakCacheRead, // $0.007 per MTok (cache hit)
SupportsCacheBreakdown: false,
}
s.fallbackPrices["deepseek-v4-flash-vision-exp"] = &ModelPricing{
InputPricePerToken: deepseekFlashOffPeakInputPrice,
OutputPricePerToken: deepseekFlashOffPeakOutputPrice,
CacheReadPricePerToken: deepseekFlashOffPeakCacheRead,
SupportsCacheBreakdown: false,
}
@@ -796,15 +848,21 @@ func (s *BillingService) getFallbackPricing(model string) *ModelPricing {
return s.fallbackPrices["gemini-3.6-flash"]
}
// DeepSeek V4 系列:仅匹配已知 V4 Pro/Flash 与官方兼容别名
// (deepseek-chat / deepseek-reasoner → V4 Flash),未知 deepseek-* 型号不回退,避免误计价。
// DeepSeek 系列:官方模型 V4 Pro/Flash(含 vision-exp)按各自价卡;
// 其余 deepseek-*(含已停服的 deepseek-chat / deepseek-reasoner 与未知型号)
// 统一按 flash 价兜底,避免计费中断。新名字由 fallback warn 日志
// (每模型每进程一条)暴露,运营者据此更新价卡。
// "deepseek-v4-flash-vision-exp" 含 "deepseek-v4-flash" 子串,显式分支置于 flash 之前,语义清晰。
if strings.Contains(modelLower, "deepseek-v4-flash-vision-exp") {
return s.fallbackPrices["deepseek-v4-flash-vision-exp"]
}
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"]
}
if strings.Contains(modelLower, "deepseek-chat") || strings.Contains(modelLower, "deepseek-reasoner") {
if strings.HasPrefix(modelLower, "deepseek-") {
return s.fallbackPrices["deepseek-v4-flash"]
}
@@ -1246,7 +1304,28 @@ func (s *BillingService) calculateTokenCost(resolved *ResolvedPricing, input Cos
return nil, fmt.Errorf("no pricing available for model: %s: %w", input.Model, ErrModelPricingUnavailable)
}
pricing = s.applyModelSpecificPricingPolicy(input.Model, pricing)
// 默认价卡(Source=LiteLLM)应用 DeepSeek 官方价强制覆盖(幂等,GetModelPricing
// 内部已强制过);分组/渠道自定义定价保留运营者配置,不强制覆盖官方价。
pricing = s.applyModelSpecificPricingPolicyEx(input.Model, pricing, resolved.Source == PricingSourceLiteLLM)
// DeepSeek 模型默认价卡按官方峰谷口径调整:高峰时段(01:00–04:00 与
// 06:00–10:00 UTC,仅工作日;北京时间周末全天低谷)按 2× 低谷价计费。
// 仅作用于默认价卡(Source=LiteLLM,无分组/渠道自定义定价)——分组/渠道
// 自定义定价保持运营者语义,不叠加。PricingAt 为零值时回退当前时刻。
// 先克隆再乘,避免污染共享 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
cloned.OutputPricePerToken *= mult
cloned.CacheReadPricePerToken *= mult
pricing = &cloned
}
}
// 官方长上下文阶梯仅在无区间定价时应用(区间定价已包含上下文分层)。
applyLongCtx := len(resolved.Intervals) == 0 && contextTierPricingEnabled
@@ -1489,10 +1568,43 @@ func (s *BillingService) calculateCostInternalWithPolicy(
return s.computeTokenBreakdown(pricing, tokens, rateMultiplier, serviceTier, longContextBillingEnabled), nil
}
// applyModelSpecificPricingPolicy 应用模型特定定价策略(GPT-5.6 长上下文/缓存写入、
// DeepSeek 官方价强制覆盖等)。默认强制 DeepSeek 官方价——该路径仅被默认价卡
// (GetModelPricing 内部)调用;分组/渠道自定义定价路径用带参数的
// applyModelSpecificPricingPolicyEx 关闭强制,保留运营者配置。
func (s *BillingService) applyModelSpecificPricingPolicy(model string, pricing *ModelPricing) *ModelPricing {
return s.applyModelSpecificPricingPolicyEx(model, pricing, true)
}
// applyModelSpecificPricingPolicyEx 与 applyModelSpecificPricingPolicy 相同,
// 但由调用方控制是否强制 DeepSeek 官方价(forceDeepSeekRates)。
// calculateTokenCost 对分组/渠道自定义定价(Source 非 LiteLLM)传 false:
// 强制覆盖会把运营者配置的售价盖回官方价,违反自定义定价语义。
func (s *BillingService) applyModelSpecificPricingPolicyEx(model string, pricing *ModelPricing, forceDeepSeekRates bool) *ModelPricing {
if pricing == nil {
return nil
}
// DeepSeek 模型:无论 JSON/远端价格表给什么价,一律强制官方低谷价
// (2026-08-23 起生效)。这是覆盖远端旧价的关键——远端仓库不可改,生产会先
// 拉到旧价,必须在此兜底修正;克隆后再覆盖,避免污染共享 fallbackPrices 指针。
// 档位判定:含 "deepseek-v4-pro" 的版本化名称(如 deepseek-v4-pro-0813)归 pro 档,
// 其余 deepseek-*(含已停服的 chat/reasoner 与未知型号)统一归 flash 档。
// 高峰时段倍率不在本函数处理,由 calculateTokenCost 按 deepseekPeakMultiplierAt
// 对默认价卡另行叠加(分组/渠道自定义定价不叠加)。
if forceDeepSeekRates && isDeepSeekModel(model) {
cloned := *pricing
if strings.Contains(strings.ToLower(strings.TrimSpace(model)), "deepseek-v4-pro") {
cloned.InputPricePerToken = deepseekProOffPeakInputPrice
cloned.OutputPricePerToken = deepseekProOffPeakOutputPrice
cloned.CacheReadPricePerToken = deepseekProOffPeakCacheRead
} else {
// deepseek-v4-flash / deepseek-v4-flash-vision-exp 与其余 deepseek-* 共用 flash 价。
cloned.InputPricePerToken = deepseekFlashOffPeakInputPrice
cloned.OutputPricePerToken = deepseekFlashOffPeakOutputPrice
cloned.CacheReadPricePerToken = deepseekFlashOffPeakCacheRead
}
return &cloned
}
normalized := normalizeKnownOpenAICodexModel(model)
isGPT56 := isOpenAIGPT56Model(normalized)
usesLegacyLongContextPricing := usesOpenAILegacyLongContextPricing(normalized)
@@ -490,30 +490,45 @@ func TestGetFallbackPricing_FamilyMatching(t *testing.T) {
{
name: "deepseek v4 pro",
model: "deepseek-v4-pro",
expectedInput: 4.35e-7,
expectedOutput: floatPtr(8.7e-7),
expectedCacheRead: floatPtr(3.625e-9),
expectedInput: 6.6e-7,
expectedOutput: floatPtr(1.98e-6),
expectedCacheRead: floatPtr(2.2e-8),
},
{
name: "deepseek v4 flash",
model: "deepseek-v4-flash",
expectedInput: 1.4e-7,
expectedOutput: floatPtr(2.8e-7),
expectedCacheRead: floatPtr(2.8e-9),
expectedInput: 2.2e-7,
expectedOutput: floatPtr(6.6e-7),
expectedCacheRead: floatPtr(7e-9),
},
{
name: "deepseek chat alias → flash",
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),
},
{
// deepseek-chat / deepseek-reasoner 已停止服务,统一按 flash 价兜底。
name: "deepseek chat discontinued maps to flash",
model: "deepseek-chat",
expectedInput: 1.4e-7,
expectedOutput: floatPtr(2.8e-7),
expectedCacheRead: floatPtr(2.8e-9),
expectedInput: 2.2e-7,
expectedOutput: floatPtr(6.6e-7),
expectedCacheRead: floatPtr(7e-9),
},
{
name: "deepseek reasoner alias → flash",
name: "deepseek reasoner discontinued maps to flash",
model: "deepseek-reasoner",
expectedInput: 1.4e-7,
expectedOutput: floatPtr(2.8e-7),
expectedCacheRead: floatPtr(2.8e-9),
expectedInput: 2.2e-7,
expectedOutput: floatPtr(6.6e-7),
expectedCacheRead: floatPtr(7e-9),
},
{
name: "unknown deepseek maps to flash",
model: "deepseek-foo",
expectedInput: 2.2e-7,
expectedOutput: floatPtr(6.6e-7),
expectedCacheRead: floatPtr(7e-9),
},
// ---- 智谱 GLM(z.ai USD 口径)----
@@ -0,0 +1,345 @@
//go:build unit
package service
import (
"context"
"os"
"path/filepath"
"testing"
"time"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
"github.com/stretchr/testify/require"
)
// ---------------------------------------------------------------------------
// deepseekPeakMultiplierAt:官方峰谷口径(2026-08-23 起生效)
// 高峰时段 01:00–04:00 与 06:00–10:00 UTC(半开区间,仅工作日);
// 北京时间周六/周日全天低谷;高峰价 = 2× 低谷价。
// 2026-08-24 为周一(工作日),2026-08-22 周六、2026-08-23 周日。
// ---------------------------------------------------------------------------
func TestDeepseekPeakMultiplierAt(t *testing.T) {
mon := func(hour, min int) time.Time { return time.Date(2026, 8, 24, hour, min, 0, 0, time.UTC) }
sat := func(hour, min int) time.Time { return time.Date(2026, 8, 22, hour, min, 0, 0, time.UTC) }
sun := func(hour, min int) time.Time { return time.Date(2026, 8, 23, hour, min, 0, 0, time.UTC) }
tests := []struct {
name string
now time.Time
want float64
}{
// 工作日高峰窗口边界(半开区间)
{"weekday 01:00 peak start", mon(1, 0), 2.0},
{"weekday 03:59 peak upper bound", mon(3, 59), 2.0},
{"weekday 04:00 peak end", mon(4, 0), 1.0},
{"weekday 06:00 peak start", mon(6, 0), 2.0},
{"weekday 09:59 peak upper bound", mon(9, 59), 2.0},
{"weekday 10:00 peak end", mon(10, 0), 1.0},
// 工作日低谷时段
{"weekday 00:00 off-peak", mon(0, 0), 1.0},
{"weekday 05:00 off-peak", mon(5, 0), 1.0},
{"weekday 12:00 off-peak", mon(12, 0), 1.0},
{"weekday 23:59 off-peak", mon(23, 59), 1.0},
// 北京时间周末全天低谷(即使 UTC 处于高峰时段)
{"saturday utc 02:00 beijing sat 10:00", sat(2, 0), 1.0},
{"sunday utc 07:00 beijing sun 15:00", sun(7, 0), 1.0},
// 北京时间与 UTC 跨日边界:UTC 周六 16:30 = 北京周日 00:30 → 周末低谷
{"utc saturday 16:30 = beijing sunday 00:30", sat(16, 30), 1.0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.want, deepseekPeakMultiplierAt(tt.now))
})
}
}
func TestIsDeepSeekModel(t *testing.T) {
deepseek := []string{
"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 ",
}
for _, m := range deepseek {
require.True(t, isDeepSeekModel(m), "model %q should be deepseek", m)
}
nonDeepseek := []string{
"gpt-5.4", "claude-sonnet-4", "deepseekcoder", // 无连字符不算 deepseek- 前缀
"", " deepseek", // 无连字符后缀
}
for _, m := range nonDeepseek {
require.False(t, isDeepSeekModel(m), "model %q should not be deepseek", m)
}
}
// ---------------------------------------------------------------------------
// 默认价卡(Source=LiteLLM)按官方峰谷倍率计费;分组/渠道自定义定价不叠加
// ---------------------------------------------------------------------------
func TestCalculateCostUnified_DeepseekDefaultCardPeakMultiplier(t *testing.T) {
bs := newTestBillingService()
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
offPeak, err := bs.CalculateCostUnified(CostInput{
Ctx: context.Background(), Model: "deepseek-v4-flash", Tokens: tokens,
RateMultiplier: 1.0, Resolver: resolver,
PricingAt: time.Date(2026, 8, 24, 12, 0, 0, 0, time.UTC), // 周一低谷
})
require.NoError(t, err)
require.InDelta(t, offPeakTotal, offPeak.TotalCost, 1e-10)
peak, err := bs.CalculateCostUnified(CostInput{
Ctx: context.Background(), Model: "deepseek-v4-flash", Tokens: tokens,
RateMultiplier: 1.0, Resolver: resolver,
PricingAt: time.Date(2026, 8, 24, 2, 0, 0, 0, time.UTC), // 周一高峰
})
require.NoError(t, err)
require.InDelta(t, offPeakTotal*2, peak.TotalCost, 1e-10)
}
func TestCalculateCostUnified_DeepseekProDefaultCardPeakMultiplier(t *testing.T) {
bs := newTestBillingService()
resolver := NewModelPricingResolver(nil, bs)
tokens := UsageTokens{InputTokens: 1000, OutputTokens: 500, CacheReadTokens: 1000}
offPeakTotal := 1000*6.6e-7 + 500*1.98e-6 + 1000*2.2e-8
offPeak, err := bs.CalculateCostUnified(CostInput{
Ctx: context.Background(), Model: "deepseek-v4-pro", Tokens: tokens,
RateMultiplier: 1.0, Resolver: resolver,
PricingAt: time.Date(2026, 8, 24, 12, 0, 0, 0, time.UTC),
})
require.NoError(t, err)
require.InDelta(t, offPeakTotal, offPeak.TotalCost, 1e-10)
peak, err := bs.CalculateCostUnified(CostInput{
Ctx: context.Background(), Model: "deepseek-v4-pro", Tokens: tokens,
RateMultiplier: 1.0, Resolver: resolver,
PricingAt: time.Date(2026, 8, 24, 6, 30, 0, 0, time.UTC), // 周一高峰
})
require.NoError(t, err)
require.InDelta(t, offPeakTotal*2, peak.TotalCost, 1e-10)
}
func TestCalculateCostUnified_DeepseekVersionedNamePeakMultiplier(t *testing.T) {
bs := newTestBillingService()
resolver := NewModelPricingResolver(nil, bs)
tokens := UsageTokens{InputTokens: 1000, OutputTokens: 500, CacheReadTokens: 1000}
offPeakTotal := 1000*2.2e-7 + 500*6.6e-7 + 1000*7e-9
offPeak, err := bs.CalculateCostUnified(CostInput{
Ctx: context.Background(), Model: "deepseek-v4-flash-0731", Tokens: tokens,
RateMultiplier: 1.0, Resolver: resolver,
PricingAt: time.Date(2026, 8, 24, 12, 0, 0, 0, time.UTC), // 周一低谷
})
require.NoError(t, err)
require.InDelta(t, offPeakTotal, offPeak.TotalCost, 1e-10)
peak, err := bs.CalculateCostUnified(CostInput{
Ctx: context.Background(), Model: "deepseek-v4-flash-0731", Tokens: tokens,
RateMultiplier: 1.0, Resolver: resolver,
PricingAt: time.Date(2026, 8, 24, 2, 0, 0, 0, time.UTC), // 周一高峰
})
require.NoError(t, err)
require.InDelta(t, offPeakTotal*2, peak.TotalCost, 1e-10)
}
func TestCalculateCostUnified_DeepseekGroupPricingNotScaledByPeak(t *testing.T) {
bs := newTestBillingService()
resolver := NewModelPricingResolver(nil, bs)
inputPrice := 1e-6
outputPrice := 2e-6
group := &Group{
ID: 1, Name: "ds-group", Platform: PlatformDeepseek, Status: StatusActive,
ModelPricing: []ChannelModelPricing{{
Models: []string{"deepseek-v4-flash"}, BillingMode: BillingModeToken,
InputPrice: &inputPrice, OutputPrice: &outputPrice,
}},
}
resolved := resolver.Resolve(context.Background(), PricingInput{Model: "deepseek-v4-flash", Group: group})
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
for _, pricingAt := range []time.Time{
time.Date(2026, 8, 24, 12, 0, 0, 0, time.UTC), // 低谷
time.Date(2026, 8, 24, 2, 0, 0, 0, time.UTC), // 高峰
} {
cost, err := bs.CalculateCostUnified(CostInput{
Ctx: context.Background(), Model: "deepseek-v4-flash", Group: group,
Tokens: tokens, RateMultiplier: 1.0, Resolver: resolver, PricingAt: pricingAt,
})
require.NoError(t, err)
require.InDelta(t, groupTotal, cost.TotalCost, 1e-10,
"分组自定义定价不应叠加官方峰谷倍率(pricingAt=%v)", pricingAt)
}
}
func TestCalculateCostUnified_NonDeepseekDefaultCardNotScaledByPeak(t *testing.T) {
bs := newTestBillingService()
resolver := NewModelPricingResolver(nil, bs)
tokens := UsageTokens{InputTokens: 1000, OutputTokens: 500}
total := 1000*3e-6 + 500*15e-6 // claude-sonnet-4 fallback
for _, pricingAt := range []time.Time{
time.Date(2026, 8, 24, 12, 0, 0, 0, time.UTC),
time.Date(2026, 8, 24, 2, 0, 0, 0, time.UTC),
} {
cost, err := bs.CalculateCostUnified(CostInput{
Ctx: context.Background(), Model: "claude-sonnet-4", Tokens: tokens,
RateMultiplier: 1.0, Resolver: resolver, PricingAt: pricingAt,
})
require.NoError(t, err)
require.InDelta(t, total, cost.TotalCost, 1e-10,
"非 DeepSeek 模型不应受官方峰谷倍率影响(pricingAt=%v)", pricingAt)
}
}
func TestCalculateCostUnified_DeepseekPricingAtZeroFallsBackToNow(t *testing.T) {
bs := newTestBillingService()
resolver := NewModelPricingResolver(nil, bs)
tokens := UsageTokens{InputTokens: 1000, OutputTokens: 500}
base := CostInput{
Ctx: context.Background(), Model: "deepseek-v4-flash", Tokens: tokens,
RateMultiplier: 1.0, Resolver: resolver,
}
// PricingAt 零值 → 回退 timezone.Now(),与显式传入当前时刻结果一致。
costZero, err := bs.CalculateCostUnified(base)
require.NoError(t, err)
costNow, err := bs.CalculateCostUnified(CostInput{
Ctx: base.Ctx, Model: base.Model, Tokens: base.Tokens,
RateMultiplier: base.RateMultiplier, Resolver: base.Resolver,
PricingAt: timezone.Now(),
})
require.NoError(t, err)
require.Equal(t, costZero.TotalCost, costNow.TotalCost)
}
// ---------------------------------------------------------------------------
// 官方价强制覆盖(远端旧价兜底)与未知 deepseek-* flash 兜底
// ---------------------------------------------------------------------------
func TestGetModelPricing_DeepseekForcesOfficialRatesOverJSON(t *testing.T) {
// JSON 给任意价(模拟远端旧价/占位价),deepseek-* 必须被强制覆盖为官方低谷价。
pricingSvc := &PricingService{pricingData: map[string]*LiteLLMModelPricing{
"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},
"deepseek-chat": {InputCostPerToken: 1e-6, OutputCostPerToken: 2e-6, CacheReadInputTokenCost: 1e-8},
"deepseek-reasoner": {InputCostPerToken: 1e-6, OutputCostPerToken: 2e-6, CacheReadInputTokenCost: 1e-8},
}}
bs := NewBillingService(&config.Config{}, pricingSvc)
tests := []struct {
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},
// 已停服的 chat/reasoner:即使 JSON 有旧条目也按 flash 价兜底。
{"deepseek-chat", 2.2e-7, 6.6e-7, 7e-9},
{"deepseek-reasoner", 2.2e-7, 6.6e-7, 7e-9},
}
for _, tt := range tests {
t.Run(tt.model, func(t *testing.T) {
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)
require.True(t, bs.HasIdentifiedTokenPricing(tt.model))
})
}
// 版本化名称(不在 JSON / fallbackPrices 精确表中):按子串归档计价。
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},
}
for _, tt := range versioned {
t.Run(tt.model, func(t *testing.T) {
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)
})
}
}
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 计费。
pricingSvc := &PricingService{pricingData: map[string]*LiteLLMModelPricing{
"deepseek-v3-2-251201": {InputCostPerToken: 0, OutputCostPerToken: 0},
}}
bs := NewBillingService(&config.Config{}, pricingSvc)
for _, m := range []string{"deepseek-v3-2-251201", "deepseek-chat", "deepseek-reasoner", "deepseek-foo"} {
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)
})
}
}
// ---------------------------------------------------------------------------
// 本地兜底 JSON:无 $0 占位条目,官方模型价格为官方低谷价
// ---------------------------------------------------------------------------
func TestDeepseekPricingFileMatchesOfficialRates(t *testing.T) {
data, err := os.ReadFile(filepath.Join("..", "..", "resources", "model-pricing", "model_prices_and_context_window.json"))
require.NoError(t, err)
pricingSvc := &PricingService{}
pricingData, err := pricingSvc.parsePricingData(data)
require.NoError(t, err)
_, ok := pricingData["deepseek-v3-2-251201"]
require.False(t, ok, "deepseek-v3-2-251201($0 占位条目)必须从价格表中移除")
for _, discontinued := range []string{"deepseek-chat", "deepseek-reasoner"} {
_, ok := pricingData[discontinued]
require.False(t, ok, "%s 已停止服务,必须从价格表中移除", discontinued)
}
tests := []struct {
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},
}
for _, tt := range tests {
t.Run(tt.model, func(t *testing.T) {
entry, ok := pricingData[tt.model]
require.True(t, ok, "model %s must exist in pricing file", tt.model)
require.InDelta(t, tt.input, entry.InputCostPerToken, 1e-15)
require.InDelta(t, tt.output, entry.OutputCostPerToken, 1e-15)
require.InDelta(t, tt.cacheRead, entry.CacheReadInputTokenCost, 1e-15)
})
}
}
@@ -745,15 +745,15 @@
"supports_web_search": true,
"supports_xhigh_reasoning_effort": true
},
"deepseek-chat": {
"cache_read_input_token_cost": 2.8e-08,
"input_cost_per_token": 2.8e-07,
"deepseek-v4-flash": {
"cache_read_input_token_cost": 7e-09,
"input_cost_per_token": 2.2e-07,
"litellm_provider": "deepseek",
"max_input_tokens": 131072,
"max_output_tokens": 8192,
"max_tokens": 8192,
"max_input_tokens": 1048576,
"max_output_tokens": 393216,
"max_tokens": 393216,
"mode": "chat",
"output_cost_per_token": 4.2e-07,
"output_cost_per_token": 6.6e-07,
"source": "https://api-docs.deepseek.com/quick_start/pricing",
"supported_endpoints": [
"/v1/chat/completions"
@@ -762,44 +762,54 @@
"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
},
"deepseek-reasoner": {
"cache_read_input_token_cost": 2.8e-08,
"input_cost_per_token": 2.8e-07,
"deepseek-v4-flash-vision-exp": {
"cache_read_input_token_cost": 7e-09,
"input_cost_per_token": 2.2e-07,
"litellm_provider": "deepseek",
"max_input_tokens": 131072,
"max_output_tokens": 65536,
"max_tokens": 65536,
"max_input_tokens": 1048576,
"max_output_tokens": 393216,
"max_tokens": 393216,
"mode": "chat",
"output_cost_per_token": 4.2e-07,
"output_cost_per_token": 6.6e-07,
"source": "https://api-docs.deepseek.com/quick_start/pricing",
"supported_endpoints": [
"/v1/chat/completions"
],
"supports_function_calling": false,
"supports_function_calling": true,
"supports_native_streaming": true,
"supports_parallel_function_calling": false,
"supports_parallel_function_calling": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": false
"supports_tool_choice": true,
"supports_vision": true
},
"deepseek-v3-2-251201": {
"input_cost_per_token": 0.0,
"litellm_provider": "volcengine",
"max_input_tokens": 98304,
"max_output_tokens": 32768,
"max_tokens": 32768,
"deepseek-v4-pro": {
"cache_read_input_token_cost": 2.2e-08,
"input_cost_per_token": 6.6e-07,
"litellm_provider": "deepseek",
"max_input_tokens": 1048576,
"max_output_tokens": 393216,
"max_tokens": 393216,
"mode": "chat",
"output_cost_per_token": 0.0,
"supports_assistant_prefill": true,
"output_cost_per_token": 1.98e-06,
"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
},
"gemini-2.0-flash": {
@@ -109,7 +109,8 @@ const qwenModels = [
// DeepSeek
const deepseekModels = [
'deepseek-chat', 'deepseek-coder', 'deepseek-reasoner',
'deepseek-v4-pro', 'deepseek-v4-flash', 'deepseek-v4-flash-vision-exp',
'deepseek-coder',
'deepseek-v3', 'deepseek-v3-0324',
'deepseek-r1', 'deepseek-r1-0528',
'deepseek-r1-distill-qwen-32b', 'deepseek-r1-distill-qwen-14b', 'deepseek-r1-distill-qwen-7b',