| 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 |