返回 DeepSeek-Reasonix
catalog_test.go
根目录 / internal / billing / catalog_test.go
1 package billing
2
3 import (
4 "testing"
5 "time"
6 )
7
8 func TestResolveDeepSeekScheduledRateBoundaries(t *testing.T) {
9 anchor := RateCard{CacheHit: 0.10, Input: 3, Output: 9, Currency: "CNY"}
10 tests := []struct {
11 name string
12 at string
13 band string
14 want RateCard
15 }{
16 {name: "legacy before cutover", at: "2026-08-16T15:59:59Z", want: RateCard{CacheHit: 0.02, Input: 1, Output: 2, Currency: "CNY"}},
17 {name: "cutover off peak", at: "2026-08-16T16:00:00Z", band: RateBandOffPeak, want: RateCard{CacheHit: 0.05, Input: 1.5, Output: 4.5, Currency: "CNY"}},
18 {name: "before morning peak", at: "2026-08-17T00:59:59Z", band: RateBandOffPeak, want: RateCard{CacheHit: 0.05, Input: 1.5, Output: 4.5, Currency: "CNY"}},
19 {name: "morning peak begins", at: "2026-08-17T01:00:00Z", band: RateBandPeak, want: anchor},
20 {name: "morning peak ends", at: "2026-08-17T04:00:00Z", band: RateBandOffPeak, want: RateCard{CacheHit: 0.05, Input: 1.5, Output: 4.5, Currency: "CNY"}},
21 {name: "afternoon peak begins", at: "2026-08-17T06:00:00Z", band: RateBandPeak, want: anchor},
22 {name: "afternoon peak ends", at: "2026-08-17T10:00:00Z", band: RateBandOffPeak, want: RateCard{CacheHit: 0.05, Input: 1.5, Output: 4.5, Currency: "CNY"}},
23 }
24 for _, tc := range tests {
25 t.Run(tc.name, func(t *testing.T) {
26 at, err := time.Parse(time.RFC3339, tc.at)
27 if err != nil {
28 t.Fatal(err)
29 }
30 if !MatchesScheduleAnchor("deepseek", "deepseek-v4-flash", ScheduleDeepSeekV4August2026, anchor) {
31 t.Fatal("current peak anchor was not recognized")
32 }
33 got, ok := ResolveScheduledRate("deepseek", "deepseek-v4-flash", "CNY", BillingModePAYG, ScheduleDeepSeekV4August2026, at)
34 if !ok || got.RateBand != tc.band || got.Card != tc.want {
35 t.Fatalf("resolved = %+v, ok=%v; want band=%q card=%+v", got, ok, tc.band, tc.want)
36 }
37 })
38 }
39 }
40
41 // The 2026-09-10 cutover both renamed the Flash SKU and cut its price; the
42 // retired ids are served by V4.1 Flash and billed at the same rate.
43 func TestDeepSeekSeptemberScheduleResolvesFlashPriceCut(t *testing.T) {
44 // Monday 2026-09-14, inside and outside the 06:00-10:00 UTC peak window.
45 peak := time.Date(2026, 9, 14, 6, 0, 0, 0, time.UTC)
46 offPeak := time.Date(2026, 9, 14, 22, 0, 0, 0, time.UTC)
47 cnyPeak := RateCard{CacheHit: 0.04, Input: 2, Output: 8, Currency: "CNY"}
48 cnyOffPeak := RateCard{CacheHit: 0.02, Input: 1, Output: 4, Currency: "CNY"}
49 for _, tc := range []struct {
50 at time.Time
51 band string
52 want RateCard
53 }{{peak, RateBandPeak, cnyPeak}, {offPeak, RateBandOffPeak, cnyOffPeak}} {
54 got, ok := ResolveScheduledRate("deepseek", "deepseek-flash", "CNY", BillingModePAYG, ScheduleDeepSeekV4September2026, tc.at)
55 if !ok || got.RateBand != tc.band || got.Card != tc.want {
56 t.Fatalf("at %s resolved = %+v ok=%v, want band=%s card=%+v", tc.at, got, ok, tc.band, tc.want)
57 }
58 }
59 for _, model := range []string{"deepseek-v4-flash", "deepseek-v4-flash-vision-exp"} {
60 got, ok := ResolveScheduledRate("deepseek", model, "CNY", BillingModePAYG, ScheduleDeepSeekV4September2026, peak)
61 if !ok || got.Card != cnyPeak {
62 t.Fatalf("%s resolved = %+v ok=%v, want the Flash price", model, got, ok)
63 }
64 }
65 if got, ok := ResolveScheduledRate("deepseek", "deepseek-flash", "USD", BillingModePAYG, ScheduleDeepSeekV4September2026, offPeak); !ok ||
66 got.Card != (RateCard{CacheHit: 0.003, Input: 0.15, Output: 0.6, Currency: "USD"}) {
67 t.Fatalf("USD off-peak resolved = %+v ok=%v", got, ok)
68 }
69 // V4 Pro keeps its own price until the vendor routes it to V4.1 Flash.
70 if got, ok := ResolveScheduledRate("deepseek", "deepseek-v4-pro", "CNY", BillingModePAYG, ScheduleDeepSeekV4September2026, peak); !ok ||
71 got.Card != (RateCard{CacheHit: 0.30, Input: 9, Output: 27, Currency: "CNY"}) {
72 t.Fatalf("V4 Pro resolved = %+v ok=%v", got, ok)
73 }
74 // The superseded August schedule closes at the cutover.
75 if _, ok := ResolveScheduledRate("deepseek", "deepseek-v4-flash", "CNY", BillingModePAYG, ScheduleDeepSeekV4August2026, peak); ok {
76 t.Fatal("August schedule still resolved after the September cutover")
77 }
78 }
79
80 func TestDeepSeekRateBandWeekendIsAlwaysOffPeak(t *testing.T) {
81 tests := []struct {
82 name string
83 at time.Time
84 }{
85 // Saturday and Sunday peak-window clock times must remain off-peak.
86 {"saturday_morning_window", time.Date(2026, 8, 22, 1, 30, 0, 0, time.UTC)},
87 {"sunday_afternoon_window", time.Date(2026, 8, 23, 7, 0, 0, 0, time.UTC)},
88 }
89 for _, tc := range tests {
90 t.Run(tc.name, func(t *testing.T) {
91 if got := DeepSeekRateBand(tc.at); got != RateBandOffPeak {
92 t.Fatalf("DeepSeekRateBand(%s) = %q, want %q", tc.at, got, RateBandOffPeak)
93 }
94 })
95 }
96 }
97
98 func TestBuildQuoteScheduledRateUsesMatchingPeerBand(t *testing.T) {
99 at := time.Date(2026, 8, 17, 0, 0, 0, 0, time.UTC)
100 q := BuildQuote(QuoteInput{
101 Usage: UsageTokens{CacheHitTokens: 1_000_000, CacheMissTokens: 1_000_000, CompletionTokens: 1_000_000},
102 Rates: RateCard{CacheHit: 0.10, Input: 3, Output: 9, Currency: "CNY"},
103 OccurredAt: at, DisplayCurrency: "USD", BillingMode: BillingModePAYG,
104 ProviderKind: "deepseek", ModelID: "deepseek-v4-flash", ScheduleID: ScheduleDeepSeekV4August2026,
105 })
106 if q.RateBand != RateBandOffPeak || q.Original.Amount != "6.05" || q.RatedAt != at.Format(time.RFC3339Nano) {
107 t.Fatalf("quote = %+v", q)
108 }
109 if q.Selected == nil || q.Selected.Currency != "USD" || q.Selected.Amount != "0.887" {
110 t.Fatalf("USD peer valuation = %+v", q.Selected)
111 }
112 peak := BuildQuote(QuoteInput{
113 Usage: UsageTokens{PromptTokens: 1_000_000},
114 Rates: RateCard{CacheHit: 0.10, Input: 3, Output: 9, Currency: "CNY"},
115 OccurredAt: time.Date(2026, 8, 17, 1, 0, 0, 0, time.UTC),
116 ProviderKind: "deepseek", ModelID: "deepseek-v4-flash", ScheduleID: ScheduleDeepSeekV4August2026,
117 })
118 if peak.RateBand != RateBandPeak || peak.Original.Amount != "3" || peak.PricingFingerprint == q.PricingFingerprint {
119 t.Fatalf("peak quote = %+v", peak)
120 }
121 }
122
123 func TestDeepSeekScheduledRatesAllModelsCurrenciesAndTokenClasses(t *testing.T) {
124 type priceSet struct {
125 model, currency string
126 anchor, peak, off RateCard
127 }
128 sets := []priceSet{
129 {"deepseek-v4-flash", "CNY", RateCard{0.10, 3, 9, "CNY"}, RateCard{0.10, 3, 9, "CNY"}, RateCard{0.05, 1.5, 4.5, "CNY"}},
130 {"deepseek-v4-flash-vision-exp", "CNY", RateCard{0.10, 3, 9, "CNY"}, RateCard{0.10, 3, 9, "CNY"}, RateCard{0.05, 1.5, 4.5, "CNY"}},
131 {"deepseek-v4-pro", "CNY", RateCard{0.30, 9, 27, "CNY"}, RateCard{0.30, 9, 27, "CNY"}, RateCard{0.15, 4.5, 13.5, "CNY"}},
132 {"deepseek-v4-flash", "USD", RateCard{0.014, 0.44, 1.32, "USD"}, RateCard{0.014, 0.44, 1.32, "USD"}, RateCard{0.007, 0.22, 0.66, "USD"}},
133 {"deepseek-v4-flash-vision-exp", "USD", RateCard{0.014, 0.44, 1.32, "USD"}, RateCard{0.014, 0.44, 1.32, "USD"}, RateCard{0.007, 0.22, 0.66, "USD"}},
134 {"deepseek-v4-pro", "USD", RateCard{0.044, 1.32, 3.96, "USD"}, RateCard{0.044, 1.32, 3.96, "USD"}, RateCard{0.022, 0.66, 1.98, "USD"}},
135 }
136 bands := []struct {
137 name string
138 at time.Time
139 band string
140 pick func(priceSet) RateCard
141 }{
142 {"peak", time.Date(2026, 8, 18, 6, 0, 0, 0, time.UTC), RateBandPeak, func(s priceSet) RateCard { return s.peak }},
143 {"off_peak_cross_utc_day", time.Date(2026, 8, 18, 23, 30, 0, 0, time.UTC), RateBandOffPeak, func(s priceSet) RateCard { return s.off }},
144 }
145 usageClasses := []struct {
146 name string
147 use UsageTokens
148 rate func(RateCard) float64
149 }{
150 {"cache_hit", UsageTokens{PromptTokens: 1_000_000, CacheHitTokens: 1_000_000}, func(r RateCard) float64 { return r.CacheHit }},
151 {"cache_miss", UsageTokens{PromptTokens: 1_000_000, CacheMissTokens: 1_000_000}, func(r RateCard) float64 { return r.Input }},
152 {"output", UsageTokens{CompletionTokens: 1_000_000}, func(r RateCard) float64 { return r.Output }},
153 }
154 for _, set := range sets {
155 for _, band := range bands {
156 for _, class := range usageClasses {
157 name := set.model + "/" + set.currency + "/" + band.name + "/" + class.name
158 t.Run(name, func(t *testing.T) {
159 q := BuildQuote(QuoteInput{
160 Usage: class.use, Rates: set.anchor, OccurredAt: band.at,
161 ProviderKind: "deepseek", ModelID: set.model, BillingMode: BillingModePAYG,
162 ScheduleID: ScheduleDeepSeekV4August2026,
163 })
164 wantCard := band.pick(set)
165 if q.RateBand != band.band || q.Original.Float64() != class.rate(wantCard) {
166 t.Fatalf("quote = %+v, want band=%s amount=%v", q, band.band, class.rate(wantCard))
167 }
168 })
169 }
170 }
171 }
172 }
173
174 func TestScheduledPricingRequiresExactAnchor(t *testing.T) {
175 q := BuildQuote(QuoteInput{
176 Usage: UsageTokens{PromptTokens: 1_000_000},
177 Rates: RateCard{Input: 99, Currency: "CNY"},
178 OccurredAt: time.Date(2026, 8, 17, 0, 0, 0, 0, time.UTC),
179 ProviderKind: "deepseek", ModelID: "deepseek-v4-flash", ScheduleID: ScheduleDeepSeekV4August2026,
180 })
181 if q.RateBand != "" || q.RatedAt != "" || q.Original.Amount != "99" {
182 t.Fatalf("custom quote was scheduled: %+v", q)
183 }
184 }
185
186 func TestStaticOffPeakLookalikeDoesNotGainOfficialValuation(t *testing.T) {
187 q := BuildQuote(QuoteInput{
188 Usage: UsageTokens{PromptTokens: 1_000_000},
189 Rates: RateCard{CacheHit: 0.05, Input: 1.5, Output: 4.5, Currency: "CNY"},
190 OccurredAt: time.Date(2026, 8, 17, 0, 0, 0, 0, time.UTC),
191 ProviderKind: "deepseek", ModelID: "deepseek-v4-flash",
192 })
193 if q.RateBand != "" || q.RatedAt != "" || q.Original.Amount != "1.5" {
194 t.Fatalf("static quote = %+v", q)
195 }
196 if _, ok := q.Valuations["USD"]; ok {
197 t.Fatalf("static off-peak lookalike gained official valuation: %+v", q.Valuations)
198 }
199 }
200
201 func TestScheduledPricingBeforeCutoverUsesHistoricalFingerprint(t *testing.T) {
202 legacy := RateCard{CacheHit: 0.02, Input: 1, Output: 2, Currency: "CNY"}
203 at := time.Date(2026, 8, 16, 15, 59, 59, 0, time.UTC)
204 q := BuildQuote(QuoteInput{
205 Usage: UsageTokens{PromptTokens: 1_000_000},
206 Rates: RateCard{CacheHit: 0.10, Input: 3, Output: 9, Currency: "CNY"},
207 OccurredAt: at, ProviderKind: "deepseek", ModelID: "deepseek-v4-flash",
208 ScheduleID: ScheduleDeepSeekV4August2026, PricingFingerprint: "caller-fingerprint",
209 })
210 if q.RateBand != "" || q.RatedAt != at.Format(time.RFC3339Nano) || q.Original.Amount != "1" {
211 t.Fatalf("legacy quote = %+v", q)
212 }
213 if q.PricingFingerprint != PricingFingerprint(legacy) {
214 t.Fatalf("fingerprint = %q, want historical rate fingerprint", q.PricingFingerprint)
215 }
216 }
217
217 lines GO