deepseek_pricing_test.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. //go:build unit
  2. package service
  3. import (
  4. "context"
  5. "os"
  6. "path/filepath"
  7. "testing"
  8. "time"
  9. "github.com/Wei-Shaw/sub2api/internal/config"
  10. "github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
  11. "github.com/stretchr/testify/require"
  12. )
  13. // ---------------------------------------------------------------------------
  14. // deepseekPeakMultiplierAt:官方峰谷口径(2026-08-23 起生效)
  15. // 高峰时段 01:00–04:00 与 06:00–10:00 UTC(半开区间,仅工作日);
  16. // 北京时间周六/周日全天低谷;高峰价 = 2× 低谷价。
  17. // 2026-08-24 为周一(工作日),2026-08-22 周六、2026-08-23 周日。
  18. // ---------------------------------------------------------------------------
  19. func TestDeepseekPeakMultiplierAt(t *testing.T) {
  20. mon := func(hour, min int) time.Time { return time.Date(2026, 8, 24, hour, min, 0, 0, time.UTC) }
  21. sat := func(hour, min int) time.Time { return time.Date(2026, 8, 22, hour, min, 0, 0, time.UTC) }
  22. sun := func(hour, min int) time.Time { return time.Date(2026, 8, 23, hour, min, 0, 0, time.UTC) }
  23. tests := []struct {
  24. name string
  25. now time.Time
  26. want float64
  27. }{
  28. // 工作日高峰窗口边界(半开区间)
  29. {"weekday 01:00 peak start", mon(1, 0), 2.0},
  30. {"weekday 03:59 peak upper bound", mon(3, 59), 2.0},
  31. {"weekday 04:00 peak end", mon(4, 0), 1.0},
  32. {"weekday 06:00 peak start", mon(6, 0), 2.0},
  33. {"weekday 09:59 peak upper bound", mon(9, 59), 2.0},
  34. {"weekday 10:00 peak end", mon(10, 0), 1.0},
  35. // 工作日低谷时段
  36. {"weekday 00:00 off-peak", mon(0, 0), 1.0},
  37. {"weekday 05:00 off-peak", mon(5, 0), 1.0},
  38. {"weekday 12:00 off-peak", mon(12, 0), 1.0},
  39. {"weekday 23:59 off-peak", mon(23, 59), 1.0},
  40. // 北京时间周末全天低谷(即使 UTC 处于高峰时段)
  41. {"saturday utc 02:00 beijing sat 10:00", sat(2, 0), 1.0},
  42. {"sunday utc 07:00 beijing sun 15:00", sun(7, 0), 1.0},
  43. // 北京时间与 UTC 跨日边界:UTC 周六 16:30 = 北京周日 00:30 → 周末低谷
  44. {"utc saturday 16:30 = beijing sunday 00:30", sat(16, 30), 1.0},
  45. }
  46. for _, tt := range tests {
  47. t.Run(tt.name, func(t *testing.T) {
  48. require.Equal(t, tt.want, deepseekPeakMultiplierAt(tt.now))
  49. })
  50. }
  51. }
  52. func TestIsDeepSeekModel(t *testing.T) {
  53. deepseek := []string{
  54. "deepseek-v4-flash", "deepseek-v4-pro", "deepseek-v4-flash-vision-exp",
  55. "deepseek-chat", "deepseek-reasoner", "deepseek-v3-2-251201",
  56. "deepseek-coder", "deepseek-foo", "deepseek-v4-pro-0813",
  57. "DEEPSEEK-V4-PRO", " deepseek-v4-flash ",
  58. }
  59. for _, m := range deepseek {
  60. require.True(t, isDeepSeekModel(m), "model %q should be deepseek", m)
  61. }
  62. nonDeepseek := []string{
  63. "gpt-5.4", "claude-sonnet-4", "deepseekcoder", // 无连字符不算 deepseek- 前缀
  64. "", " deepseek", // 无连字符后缀
  65. }
  66. for _, m := range nonDeepseek {
  67. require.False(t, isDeepSeekModel(m), "model %q should not be deepseek", m)
  68. }
  69. }
  70. // ---------------------------------------------------------------------------
  71. // 默认价卡(Source=LiteLLM)按官方峰谷倍率计费;分组/渠道自定义定价不叠加
  72. // ---------------------------------------------------------------------------
  73. func TestCalculateCostUnified_DeepseekDefaultCardPeakMultiplier(t *testing.T) {
  74. bs := newTestBillingService()
  75. resolver := NewModelPricingResolver(nil, bs)
  76. tokens := UsageTokens{InputTokens: 1000, OutputTokens: 500, CacheReadTokens: 1000}
  77. // 低谷成本:1000*2.2e-7 + 500*6.6e-7 + 1000*7e-9 = 5.57e-4
  78. offPeakTotal := 1000*2.2e-7 + 500*6.6e-7 + 1000*7e-9
  79. offPeak, err := bs.CalculateCostUnified(CostInput{
  80. Ctx: context.Background(), Model: "deepseek-v4-flash", Tokens: tokens,
  81. RateMultiplier: 1.0, Resolver: resolver,
  82. PricingAt: time.Date(2026, 8, 24, 12, 0, 0, 0, time.UTC), // 周一低谷
  83. })
  84. require.NoError(t, err)
  85. require.InDelta(t, offPeakTotal, offPeak.TotalCost, 1e-10)
  86. peak, err := bs.CalculateCostUnified(CostInput{
  87. Ctx: context.Background(), Model: "deepseek-v4-flash", Tokens: tokens,
  88. RateMultiplier: 1.0, Resolver: resolver,
  89. PricingAt: time.Date(2026, 8, 24, 2, 0, 0, 0, time.UTC), // 周一高峰
  90. })
  91. require.NoError(t, err)
  92. require.InDelta(t, offPeakTotal*2, peak.TotalCost, 1e-10)
  93. }
  94. func TestCalculateCostUnified_DeepseekProDefaultCardPeakMultiplier(t *testing.T) {
  95. bs := newTestBillingService()
  96. resolver := NewModelPricingResolver(nil, bs)
  97. tokens := UsageTokens{InputTokens: 1000, OutputTokens: 500, CacheReadTokens: 1000}
  98. offPeakTotal := 1000*6.6e-7 + 500*1.98e-6 + 1000*2.2e-8
  99. offPeak, err := bs.CalculateCostUnified(CostInput{
  100. Ctx: context.Background(), Model: "deepseek-v4-pro", Tokens: tokens,
  101. RateMultiplier: 1.0, Resolver: resolver,
  102. PricingAt: time.Date(2026, 8, 24, 12, 0, 0, 0, time.UTC),
  103. })
  104. require.NoError(t, err)
  105. require.InDelta(t, offPeakTotal, offPeak.TotalCost, 1e-10)
  106. peak, err := bs.CalculateCostUnified(CostInput{
  107. Ctx: context.Background(), Model: "deepseek-v4-pro", Tokens: tokens,
  108. RateMultiplier: 1.0, Resolver: resolver,
  109. PricingAt: time.Date(2026, 8, 24, 6, 30, 0, 0, time.UTC), // 周一高峰
  110. })
  111. require.NoError(t, err)
  112. require.InDelta(t, offPeakTotal*2, peak.TotalCost, 1e-10)
  113. }
  114. func TestCalculateCostUnified_DeepseekVersionedNamePeakMultiplier(t *testing.T) {
  115. bs := newTestBillingService()
  116. resolver := NewModelPricingResolver(nil, bs)
  117. tokens := UsageTokens{InputTokens: 1000, OutputTokens: 500, CacheReadTokens: 1000}
  118. offPeakTotal := 1000*2.2e-7 + 500*6.6e-7 + 1000*7e-9
  119. offPeak, err := bs.CalculateCostUnified(CostInput{
  120. Ctx: context.Background(), Model: "deepseek-v4-flash-0731", Tokens: tokens,
  121. RateMultiplier: 1.0, Resolver: resolver,
  122. PricingAt: time.Date(2026, 8, 24, 12, 0, 0, 0, time.UTC), // 周一低谷
  123. })
  124. require.NoError(t, err)
  125. require.InDelta(t, offPeakTotal, offPeak.TotalCost, 1e-10)
  126. peak, err := bs.CalculateCostUnified(CostInput{
  127. Ctx: context.Background(), Model: "deepseek-v4-flash-0731", Tokens: tokens,
  128. RateMultiplier: 1.0, Resolver: resolver,
  129. PricingAt: time.Date(2026, 8, 24, 2, 0, 0, 0, time.UTC), // 周一高峰
  130. })
  131. require.NoError(t, err)
  132. require.InDelta(t, offPeakTotal*2, peak.TotalCost, 1e-10)
  133. }
  134. func TestCalculateCostUnified_DeepseekGroupPricingNotScaledByPeak(t *testing.T) {
  135. bs := newTestBillingService()
  136. resolver := NewModelPricingResolver(nil, bs)
  137. inputPrice := 1e-6
  138. outputPrice := 2e-6
  139. group := &Group{
  140. ID: 1, Name: "ds-group", Platform: PlatformDeepseek, Status: StatusActive,
  141. ModelPricing: []ChannelModelPricing{{
  142. Models: []string{"deepseek-v4-flash"}, BillingMode: BillingModeToken,
  143. InputPrice: &inputPrice, OutputPrice: &outputPrice,
  144. }},
  145. }
  146. resolved := resolver.Resolve(context.Background(), PricingInput{Model: "deepseek-v4-flash", Group: group})
  147. require.Equal(t, PricingSourceGroup, resolved.Source)
  148. tokens := UsageTokens{InputTokens: 1000, OutputTokens: 500, CacheReadTokens: 1000}
  149. // 分组自定义价:1000*1e-6 + 500*2e-6 + 1000*7e-9(缓存读沿用官方 flash 价)
  150. groupTotal := 1000*1e-6 + 500*2e-6 + 1000*7e-9
  151. for _, pricingAt := range []time.Time{
  152. time.Date(2026, 8, 24, 12, 0, 0, 0, time.UTC), // 低谷
  153. time.Date(2026, 8, 24, 2, 0, 0, 0, time.UTC), // 高峰
  154. } {
  155. cost, err := bs.CalculateCostUnified(CostInput{
  156. Ctx: context.Background(), Model: "deepseek-v4-flash", Group: group,
  157. Tokens: tokens, RateMultiplier: 1.0, Resolver: resolver, PricingAt: pricingAt,
  158. })
  159. require.NoError(t, err)
  160. require.InDelta(t, groupTotal, cost.TotalCost, 1e-10,
  161. "分组自定义定价不应叠加官方峰谷倍率(pricingAt=%v)", pricingAt)
  162. }
  163. }
  164. func TestCalculateCostUnified_NonDeepseekDefaultCardNotScaledByPeak(t *testing.T) {
  165. bs := newTestBillingService()
  166. resolver := NewModelPricingResolver(nil, bs)
  167. tokens := UsageTokens{InputTokens: 1000, OutputTokens: 500}
  168. total := 1000*3e-6 + 500*15e-6 // claude-sonnet-4 fallback
  169. for _, pricingAt := range []time.Time{
  170. time.Date(2026, 8, 24, 12, 0, 0, 0, time.UTC),
  171. time.Date(2026, 8, 24, 2, 0, 0, 0, time.UTC),
  172. } {
  173. cost, err := bs.CalculateCostUnified(CostInput{
  174. Ctx: context.Background(), Model: "claude-sonnet-4", Tokens: tokens,
  175. RateMultiplier: 1.0, Resolver: resolver, PricingAt: pricingAt,
  176. })
  177. require.NoError(t, err)
  178. require.InDelta(t, total, cost.TotalCost, 1e-10,
  179. "非 DeepSeek 模型不应受官方峰谷倍率影响(pricingAt=%v)", pricingAt)
  180. }
  181. }
  182. func TestCalculateCostUnified_DeepseekPricingAtZeroFallsBackToNow(t *testing.T) {
  183. bs := newTestBillingService()
  184. resolver := NewModelPricingResolver(nil, bs)
  185. tokens := UsageTokens{InputTokens: 1000, OutputTokens: 500}
  186. base := CostInput{
  187. Ctx: context.Background(), Model: "deepseek-v4-flash", Tokens: tokens,
  188. RateMultiplier: 1.0, Resolver: resolver,
  189. }
  190. // PricingAt 零值 → 回退 timezone.Now(),与显式传入当前时刻结果一致。
  191. costZero, err := bs.CalculateCostUnified(base)
  192. require.NoError(t, err)
  193. costNow, err := bs.CalculateCostUnified(CostInput{
  194. Ctx: base.Ctx, Model: base.Model, Tokens: base.Tokens,
  195. RateMultiplier: base.RateMultiplier, Resolver: base.Resolver,
  196. PricingAt: timezone.Now(),
  197. })
  198. require.NoError(t, err)
  199. require.Equal(t, costZero.TotalCost, costNow.TotalCost)
  200. }
  201. // ---------------------------------------------------------------------------
  202. // 官方价强制覆盖(远端旧价兜底)与未知 deepseek-* flash 兜底
  203. // ---------------------------------------------------------------------------
  204. func TestGetModelPricing_DeepseekForcesOfficialRatesOverJSON(t *testing.T) {
  205. // JSON 给任意价(模拟远端旧价/占位价),deepseek-* 必须被强制覆盖为官方低谷价。
  206. pricingSvc := &PricingService{pricingData: map[string]*LiteLLMModelPricing{
  207. "deepseek-v4-flash": {InputCostPerToken: 1e-6, OutputCostPerToken: 2e-6, CacheReadInputTokenCost: 1e-8},
  208. "deepseek-v4-pro": {InputCostPerToken: 1e-6, OutputCostPerToken: 2e-6, CacheReadInputTokenCost: 1e-8},
  209. "deepseek-v4-flash-vision-exp": {InputCostPerToken: 1e-6, OutputCostPerToken: 2e-6, CacheReadInputTokenCost: 1e-8},
  210. "deepseek-chat": {InputCostPerToken: 1e-6, OutputCostPerToken: 2e-6, CacheReadInputTokenCost: 1e-8},
  211. "deepseek-reasoner": {InputCostPerToken: 1e-6, OutputCostPerToken: 2e-6, CacheReadInputTokenCost: 1e-8},
  212. }}
  213. bs := NewBillingService(&config.Config{}, pricingSvc)
  214. tests := []struct {
  215. model string
  216. input, output, cacheRead float64
  217. }{
  218. {"deepseek-v4-flash", 2.2e-7, 6.6e-7, 7e-9},
  219. {"deepseek-v4-flash-vision-exp", 2.2e-7, 6.6e-7, 7e-9},
  220. {"deepseek-v4-pro", 6.6e-7, 1.98e-6, 2.2e-8},
  221. // 已停服的 chat/reasoner:即使 JSON 有旧条目也按 flash 价兜底。
  222. {"deepseek-chat", 2.2e-7, 6.6e-7, 7e-9},
  223. {"deepseek-reasoner", 2.2e-7, 6.6e-7, 7e-9},
  224. }
  225. for _, tt := range tests {
  226. t.Run(tt.model, func(t *testing.T) {
  227. pricing, err := bs.GetModelPricing(tt.model)
  228. require.NoError(t, err)
  229. require.InDelta(t, tt.input, pricing.InputPricePerToken, 1e-15)
  230. require.InDelta(t, tt.output, pricing.OutputPricePerToken, 1e-15)
  231. require.InDelta(t, tt.cacheRead, pricing.CacheReadPricePerToken, 1e-15)
  232. require.True(t, bs.HasIdentifiedTokenPricing(tt.model))
  233. })
  234. }
  235. // 版本化名称(不在 JSON / fallbackPrices 精确表中):按子串归档计价。
  236. versioned := []struct {
  237. model string
  238. input, output, cacheRead float64
  239. }{
  240. {"deepseek-v4-pro-0813", 6.6e-7, 1.98e-6, 2.2e-8},
  241. {"deepseek-v4-flash-0731", 2.2e-7, 6.6e-7, 7e-9},
  242. }
  243. for _, tt := range versioned {
  244. t.Run(tt.model, func(t *testing.T) {
  245. pricing, err := bs.GetModelPricing(tt.model)
  246. require.NoError(t, err)
  247. require.InDelta(t, tt.input, pricing.InputPricePerToken, 1e-15)
  248. require.InDelta(t, tt.output, pricing.OutputPricePerToken, 1e-15)
  249. require.InDelta(t, tt.cacheRead, pricing.CacheReadPricePerToken, 1e-15)
  250. })
  251. }
  252. }
  253. func TestGetModelPricing_UnknownDeepseekMapsToFlash(t *testing.T) {
  254. // JSON 含 $0 占位条目(如旧 deepseek-v3-2-251201):未知 deepseek-* 不再
  255. // fail-closed,统一按 flash 价兜底(2.2e-7/6.6e-7/7e-9),不得按 $0 计费。
  256. pricingSvc := &PricingService{pricingData: map[string]*LiteLLMModelPricing{
  257. "deepseek-v3-2-251201": {InputCostPerToken: 0, OutputCostPerToken: 0},
  258. }}
  259. bs := NewBillingService(&config.Config{}, pricingSvc)
  260. for _, m := range []string{"deepseek-v3-2-251201", "deepseek-chat", "deepseek-reasoner", "deepseek-foo"} {
  261. t.Run(m, func(t *testing.T) {
  262. pricing, err := bs.GetModelPricing(m)
  263. require.NoError(t, err)
  264. require.InDelta(t, 2.2e-7, pricing.InputPricePerToken, 1e-15)
  265. require.InDelta(t, 6.6e-7, pricing.OutputPricePerToken, 1e-15)
  266. require.InDelta(t, 7e-9, pricing.CacheReadPricePerToken, 1e-15)
  267. })
  268. }
  269. }
  270. // ---------------------------------------------------------------------------
  271. // 本地兜底 JSON:无 $0 占位条目,官方模型价格为官方低谷价
  272. // ---------------------------------------------------------------------------
  273. func TestDeepseekPricingFileMatchesOfficialRates(t *testing.T) {
  274. data, err := os.ReadFile(filepath.Join("..", "..", "resources", "model-pricing", "model_prices_and_context_window.json"))
  275. require.NoError(t, err)
  276. pricingSvc := &PricingService{}
  277. pricingData, err := pricingSvc.parsePricingData(data)
  278. require.NoError(t, err)
  279. _, ok := pricingData["deepseek-v3-2-251201"]
  280. require.False(t, ok, "deepseek-v3-2-251201($0 占位条目)必须从价格表中移除")
  281. for _, discontinued := range []string{"deepseek-chat", "deepseek-reasoner"} {
  282. _, ok := pricingData[discontinued]
  283. require.False(t, ok, "%s 已停止服务,必须从价格表中移除", discontinued)
  284. }
  285. tests := []struct {
  286. model string
  287. input, output, cacheRead float64
  288. }{
  289. {"deepseek-v4-flash", 2.2e-7, 6.6e-7, 7e-9},
  290. {"deepseek-v4-flash-vision-exp", 2.2e-7, 6.6e-7, 7e-9},
  291. {"deepseek-v4-pro", 6.6e-7, 1.98e-6, 2.2e-8},
  292. }
  293. for _, tt := range tests {
  294. t.Run(tt.model, func(t *testing.T) {
  295. entry, ok := pricingData[tt.model]
  296. require.True(t, ok, "model %s must exist in pricing file", tt.model)
  297. require.InDelta(t, tt.input, entry.InputCostPerToken, 1e-15)
  298. require.InDelta(t, tt.output, entry.OutputCostPerToken, 1e-15)
  299. require.InDelta(t, tt.cacheRead, entry.CacheReadInputTokenCost, 1e-15)
  300. })
  301. }
  302. }