| 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 TestDeepSeekRateBandChinesePublicHolidaysAreOffPeak(t *testing.T) { |
| 99 | tests := []struct { |
| 100 | name string |
| 101 | at time.Time |
| 102 | }{ |
| 103 | // National Day Thursday 2026-10-01 inside the morning peak window |
| 104 | // (Beijing 10:00). |
| 105 | {"national_day_morning_window", time.Date(2026, 10, 1, 2, 0, 0, 0, time.UTC)}, |
| 106 | // Mid-Autumn Friday 2026-09-25 inside the afternoon peak window |
| 107 | // (Beijing 15:00). |
| 108 | {"mid_autumn_afternoon_window", time.Date(2026, 9, 25, 7, 0, 0, 0, time.UTC)}, |
| 109 | // Spring Festival Monday 2026-02-17 (正月初一) inside the morning peak |
| 110 | // window (Beijing 09:30). |
| 111 | {"spring_festival_first_day", time.Date(2026, 2, 17, 1, 30, 0, 0, time.UTC)}, |
| 112 | // The Beijing evening before a holiday starts (09-24 20:00 local) is a |
| 113 | // plain Thursday night and stays off-peak through the window rule. |
| 114 | {"night_before_holiday", time.Date(2026, 9, 24, 12, 0, 0, 0, time.UTC)}, |
| 115 | } |
| 116 | for _, tc := range tests { |
| 117 | t.Run(tc.name, func(t *testing.T) { |
| 118 | if got := DeepSeekRateBand(tc.at); got != RateBandOffPeak { |
| 119 | t.Fatalf("DeepSeekRateBand(%s) = %q, want %q", tc.at, got, RateBandOffPeak) |
| 120 | } |
| 121 | }) |
| 122 | } |
| 123 | } |
| 124 | |
| 125 | func TestDeepSeekRateBandMakeupWorkdayWeekendStaysOffPeak(t *testing.T) { |
| 126 | // 2026-10-10 is the National Day make-up workday (a Saturday): the pricing |
| 127 | // page keys off calendar weekends, so it still bills off-peak. |
| 128 | at := time.Date(2026, 10, 10, 2, 0, 0, 0, time.UTC) |
| 129 | if got := DeepSeekRateBand(at); got != RateBandOffPeak { |
| 130 | t.Fatalf("DeepSeekRateBand(%s) = %q, want %q", at, got, RateBandOffPeak) |
| 131 | } |
| 132 | } |
| 133 | |
| 134 | func TestDeepSeekRateBandOrdinaryWeekdayStillPeaks(t *testing.T) { |
| 135 | // Control: a normal weekday inside both peak windows keeps billing peak. |
| 136 | for _, at := range []time.Time{ |
| 137 | time.Date(2026, 9, 24, 1, 0, 0, 0, time.UTC), // Beijing 09:00 |
| 138 | time.Date(2026, 10, 13, 9, 59, 0, 0, time.UTC), // Beijing 17:59 |
| 139 | } { |
| 140 | if got := DeepSeekRateBand(at); got != RateBandPeak { |
| 141 | t.Fatalf("DeepSeekRateBand(%s) = %q, want %q", at, got, RateBandPeak) |
| 142 | } |
| 143 | } |
| 144 | } |
| 145 | |
| 146 | func TestBuildQuoteScheduledRateUsesMatchingPeerBand(t *testing.T) { |
| 147 | at := time.Date(2026, 8, 17, 0, 0, 0, 0, time.UTC) |
| 148 | q := BuildQuote(QuoteInput{ |
| 149 | Usage: UsageTokens{CacheHitTokens: 1_000_000, CacheMissTokens: 1_000_000, CompletionTokens: 1_000_000}, |
| 150 | Rates: RateCard{CacheHit: 0.10, Input: 3, Output: 9, Currency: "CNY"}, |
| 151 | OccurredAt: at, DisplayCurrency: "USD", BillingMode: BillingModePAYG, |
| 152 | ProviderKind: "deepseek", ModelID: "deepseek-v4-flash", ScheduleID: ScheduleDeepSeekV4August2026, |
| 153 | }) |
| 154 | if q.RateBand != RateBandOffPeak || q.Original.Amount != "6.05" || q.RatedAt != at.Format(time.RFC3339Nano) { |
| 155 | t.Fatalf("quote = %+v", q) |
| 156 | } |
| 157 | if q.Selected == nil || q.Selected.Currency != "USD" || q.Selected.Amount != "0.887" { |
| 158 | t.Fatalf("USD peer valuation = %+v", q.Selected) |
| 159 | } |
| 160 | peak := BuildQuote(QuoteInput{ |
| 161 | Usage: UsageTokens{PromptTokens: 1_000_000}, |
| 162 | Rates: RateCard{CacheHit: 0.10, Input: 3, Output: 9, Currency: "CNY"}, |
| 163 | OccurredAt: time.Date(2026, 8, 17, 1, 0, 0, 0, time.UTC), |
| 164 | ProviderKind: "deepseek", ModelID: "deepseek-v4-flash", ScheduleID: ScheduleDeepSeekV4August2026, |
| 165 | }) |
| 166 | if peak.RateBand != RateBandPeak || peak.Original.Amount != "3" || peak.PricingFingerprint == q.PricingFingerprint { |
| 167 | t.Fatalf("peak quote = %+v", peak) |
| 168 | } |
| 169 | } |
| 170 | |
| 171 | func TestDeepSeekScheduledRatesAllModelsCurrenciesAndTokenClasses(t *testing.T) { |
| 172 | type priceSet struct { |
| 173 | model, currency string |
| 174 | anchor, peak, off RateCard |
| 175 | } |
| 176 | sets := []priceSet{ |
| 177 | {"deepseek-v4-flash", "CNY", RateCard{0.10, 3, 9, "CNY"}, RateCard{0.10, 3, 9, "CNY"}, RateCard{0.05, 1.5, 4.5, "CNY"}}, |
| 178 | {"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"}}, |
| 179 | {"deepseek-v4-pro", "CNY", RateCard{0.30, 9, 27, "CNY"}, RateCard{0.30, 9, 27, "CNY"}, RateCard{0.15, 4.5, 13.5, "CNY"}}, |
| 180 | {"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"}}, |
| 181 | {"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"}}, |
| 182 | {"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"}}, |
| 183 | } |
| 184 | bands := []struct { |
| 185 | name string |
| 186 | at time.Time |
| 187 | band string |
| 188 | pick func(priceSet) RateCard |
| 189 | }{ |
| 190 | {"peak", time.Date(2026, 8, 18, 6, 0, 0, 0, time.UTC), RateBandPeak, func(s priceSet) RateCard { return s.peak }}, |
| 191 | {"off_peak_cross_utc_day", time.Date(2026, 8, 18, 23, 30, 0, 0, time.UTC), RateBandOffPeak, func(s priceSet) RateCard { return s.off }}, |
| 192 | } |
| 193 | usageClasses := []struct { |
| 194 | name string |
| 195 | use UsageTokens |
| 196 | rate func(RateCard) float64 |
| 197 | }{ |
| 198 | {"cache_hit", UsageTokens{PromptTokens: 1_000_000, CacheHitTokens: 1_000_000}, func(r RateCard) float64 { return r.CacheHit }}, |
| 199 | {"cache_miss", UsageTokens{PromptTokens: 1_000_000, CacheMissTokens: 1_000_000}, func(r RateCard) float64 { return r.Input }}, |
| 200 | {"output", UsageTokens{CompletionTokens: 1_000_000}, func(r RateCard) float64 { return r.Output }}, |
| 201 | } |
| 202 | for _, set := range sets { |
| 203 | for _, band := range bands { |
| 204 | for _, class := range usageClasses { |
| 205 | name := set.model + "/" + set.currency + "/" + band.name + "/" + class.name |
| 206 | t.Run(name, func(t *testing.T) { |
| 207 | q := BuildQuote(QuoteInput{ |
| 208 | Usage: class.use, Rates: set.anchor, OccurredAt: band.at, |
| 209 | ProviderKind: "deepseek", ModelID: set.model, BillingMode: BillingModePAYG, |
| 210 | ScheduleID: ScheduleDeepSeekV4August2026, |
| 211 | }) |
| 212 | wantCard := band.pick(set) |
| 213 | if q.RateBand != band.band || q.Original.Float64() != class.rate(wantCard) { |
| 214 | t.Fatalf("quote = %+v, want band=%s amount=%v", q, band.band, class.rate(wantCard)) |
| 215 | } |
| 216 | }) |
| 217 | } |
| 218 | } |
| 219 | } |
| 220 | } |
| 221 | |
| 222 | func TestScheduledPricingRequiresExactAnchor(t *testing.T) { |
| 223 | q := BuildQuote(QuoteInput{ |
| 224 | Usage: UsageTokens{PromptTokens: 1_000_000}, |
| 225 | Rates: RateCard{Input: 99, Currency: "CNY"}, |
| 226 | OccurredAt: time.Date(2026, 8, 17, 0, 0, 0, 0, time.UTC), |
| 227 | ProviderKind: "deepseek", ModelID: "deepseek-v4-flash", ScheduleID: ScheduleDeepSeekV4August2026, |
| 228 | }) |
| 229 | if q.RateBand != "" || q.RatedAt != "" || q.Original.Amount != "99" { |
| 230 | t.Fatalf("custom quote was scheduled: %+v", q) |
| 231 | } |
| 232 | } |
| 233 | |
| 234 | func TestStaticOffPeakLookalikeDoesNotGainOfficialValuation(t *testing.T) { |
| 235 | q := BuildQuote(QuoteInput{ |
| 236 | Usage: UsageTokens{PromptTokens: 1_000_000}, |
| 237 | Rates: RateCard{CacheHit: 0.05, Input: 1.5, Output: 4.5, Currency: "CNY"}, |
| 238 | OccurredAt: time.Date(2026, 8, 17, 0, 0, 0, 0, time.UTC), |
| 239 | ProviderKind: "deepseek", ModelID: "deepseek-v4-flash", |
| 240 | }) |
| 241 | if q.RateBand != "" || q.RatedAt != "" || q.Original.Amount != "1.5" { |
| 242 | t.Fatalf("static quote = %+v", q) |
| 243 | } |
| 244 | if _, ok := q.Valuations["USD"]; ok { |
| 245 | t.Fatalf("static off-peak lookalike gained official valuation: %+v", q.Valuations) |
| 246 | } |
| 247 | } |
| 248 | |
| 249 | func TestScheduledPricingBeforeCutoverUsesHistoricalFingerprint(t *testing.T) { |
| 250 | legacy := RateCard{CacheHit: 0.02, Input: 1, Output: 2, Currency: "CNY"} |
| 251 | at := time.Date(2026, 8, 16, 15, 59, 59, 0, time.UTC) |
| 252 | q := BuildQuote(QuoteInput{ |
| 253 | Usage: UsageTokens{PromptTokens: 1_000_000}, |
| 254 | Rates: RateCard{CacheHit: 0.10, Input: 3, Output: 9, Currency: "CNY"}, |
| 255 | OccurredAt: at, ProviderKind: "deepseek", ModelID: "deepseek-v4-flash", |
| 256 | ScheduleID: ScheduleDeepSeekV4August2026, PricingFingerprint: "caller-fingerprint", |
| 257 | }) |
| 258 | if q.RateBand != "" || q.RatedAt != at.Format(time.RFC3339Nano) || q.Original.Amount != "1" { |
| 259 | t.Fatalf("legacy quote = %+v", q) |
| 260 | } |
| 261 | if q.PricingFingerprint != PricingFingerprint(legacy) { |
| 262 | t.Fatalf("fingerprint = %q, want historical rate fingerprint", q.PricingFingerprint) |
| 263 | } |
| 264 | } |
| 265 |