diff --git a/gateway/internal/meter/meter.go b/gateway/internal/meter/meter.go index 3bcdf49..bca50a7 100644 --- a/gateway/internal/meter/meter.go +++ b/gateway/internal/meter/meter.go @@ -48,6 +48,20 @@ type Price struct { // copy does. var RepriceAt = time.Date(2026, time.August, 16, 16, 0, 0, 0, time.UTC) +// weekendOffPeakAt is when weekends stopped billing peak: 16:00 UTC on +// 2026-08-22 (00:00 Beijing, Sun 23 Aug). A Saturday or Sunday on the +// Beijing calendar is off-peak for all 24 hours from that instant. +// +// The drift here runs the other way to the repricing's: over-charging our +// own budget is safe for the credit pool but it is still wrong, and it +// makes /economics report a cost-per-task the account never paid. The +// same date gates the CLI's copy; `make price-check` guards the numbers, +// and this comment is the only thing guarding the clock. +var weekendOffPeakAt = time.Date(2026, time.August, 22, 16, 0, 0, 0, time.UTC) + +// beijing is the vendor's clock; no daylight saving since 1991. +var beijing = time.FixedZone("CST", 8*60*60) + // ratesFlat is the card published 2026-08-02, in force before RepriceAt. var ratesFlat = map[string]Price{ "deepseek-v4-flash": {CacheHitInput: 0.0028, CacheMissInput: 0.14, Output: 0.28}, @@ -58,16 +72,25 @@ var ratesFlat = map[string]Price{ // every billing item costs peakMultiplier times these numbers. var ratesOffPeak = map[string]Price{ "deepseek-v4-flash": {CacheHitInput: 0.007, CacheMissInput: 0.22, Output: 0.66}, - "deepseek-v4-pro": {CacheHitInput: 0.022, CacheMissInput: 0.66, Output: 1.98}, + // Released 2026-08-21, after the switchover, so it has no flat row + // above. Priced identically to flash. Carried here even though the + // policy allowlist does not admit it, so that a metering gap can never + // be the reason it gets served for free. + "deepseek-v4-flash-vision-exp": {CacheHitInput: 0.007, CacheMissInput: 0.22, Output: 0.66}, + "deepseek-v4-pro": {CacheHitInput: 0.022, CacheMissInput: 0.66, Output: 1.98}, } const peakMultiplier = 2.0 -// peakWindows are the daily peak hours from RepriceAt on, in minutes of -// the UTC day, end exclusive: 01:00-04:00 and 06:00-10:00 UTC. +// peakWindows are the peak hours from RepriceAt on, in minutes of the UTC +// day, end exclusive: 01:00-04:00 and 06:00-10:00 UTC. Every day until +// weekendOffPeakAt, weekdays only after it. var peakWindows = [][2]int{{1 * 60, 4 * 60}, {6 * 60, 10 * 60}} func inPeak(t time.Time) bool { + if !t.Before(weekendOffPeakAt) && isBeijingWeekend(t) { + return false + } u := t.UTC() m := u.Hour()*60 + u.Minute() for _, w := range peakWindows { @@ -78,6 +101,17 @@ func inPeak(t time.Time) bool { return false } +// isBeijingWeekend reports whether an instant is a Saturday or Sunday in +// Beijing. Read on the vendor's clock because that is how the rule is +// published, which puts the turnover at 16:00 UTC, not midnight UTC. +func isBeijingWeekend(t time.Time) bool { + switch t.In(beijing).Weekday() { + case time.Saturday, time.Sunday: + return true + } + return false +} + // PriceFor returns the rate card in effect right now, defaulting to the // more expensive model. Charging an unknown model at pro rates is // deliberate: if DeepSeek ships a third model and we have not updated diff --git a/gateway/internal/meter/weekend_test.go b/gateway/internal/meter/weekend_test.go new file mode 100644 index 0000000..6d31795 --- /dev/null +++ b/gateway/internal/meter/weekend_test.go @@ -0,0 +1,63 @@ +package meter + +import ( + "testing" + "time" +) + +// The gateway keeps its own copy of the rate card and the clock, because +// it is a separate module. `make price-check` guards the numbers from +// drifting against the CLI's copy; nothing guards the clock, so these +// pin it here too. +func TestWeekendsAreOffPeak(t *testing.T) { + cases := []struct { + at time.Time + peak bool + why string + }{ + {time.Date(2026, 8, 28, 2, 0, 0, 0, time.UTC), true, "Friday, inside the first window"}, + {time.Date(2026, 8, 29, 2, 0, 0, 0, time.UTC), false, "Saturday, same window"}, + {time.Date(2026, 8, 30, 2, 0, 0, 0, time.UTC), false, "Sunday, same window"}, + {time.Date(2026, 8, 31, 2, 0, 0, 0, time.UTC), true, "Monday, same window"}, + // The rule started 2026-08-22 16:00 UTC. Saturday 2026-08-22 is the + // only weekend day that ever billed peak under time-of-use, and + // over-charging our own budget for it is still what actually happened. + {time.Date(2026, 8, 22, 2, 0, 0, 0, time.UTC), true, "Saturday before the rule"}, + {time.Date(2026, 8, 23, 2, 0, 0, 0, time.UTC), false, "Sunday, first under the rule"}, + } + for _, c := range cases { + if got := inPeak(c.at); got != c.peak { + t.Errorf("%s (%v): inPeak = %v, want %v", c.why, c.at, got, c.peak) + } + } +} + +func TestWeekendIsReadOnTheVendorClock(t *testing.T) { + // Both windows close at 10:00 UTC, before the 16:00 UTC point where a + // UTC date and a Beijing date diverge -- so no billed instant tells a + // UTC reading from a Beijing one, and every test written against the + // windows passes with the shift deleted. Only these two catch it. + if !isBeijingWeekend(time.Date(2026, 8, 28, 16, 30, 0, 0, time.UTC)) { + t.Error("Friday 16:30 UTC is already Saturday in Beijing") + } + if isBeijingWeekend(time.Date(2026, 8, 28, 15, 30, 0, 0, time.UTC)) { + t.Error("Friday 15:30 UTC is still Friday in Beijing") + } + if isBeijingWeekend(time.Date(2026, 8, 30, 16, 30, 0, 0, time.UTC)) { + t.Error("Sunday 16:30 UTC is already Monday in Beijing") + } +} + +func TestVisionVariantIsMetered(t *testing.T) { + // A model priced upstream but missing from this card would be served + // for free out of the budget pool. + at := time.Date(2026, 8, 26, 12, 0, 0, 0, time.UTC) + vision := PriceAt("deepseek-v4-flash-vision-exp", at) + flash := PriceAt("deepseek-v4-flash", at) + if vision != flash { + t.Errorf("vision %+v != flash %+v", vision, flash) + } + if vision.CacheMissInput == 0 { + t.Error("vision meters at zero") + } +} diff --git a/internal/cli/pricing.go b/internal/cli/pricing.go index 805dfc5..969aebc 100644 --- a/internal/cli/pricing.go +++ b/internal/cli/pricing.go @@ -56,7 +56,9 @@ Answer what a token costs at this instant, and when that changes. DeepSeek's repricing of 2026-08-13 is dated: until 16:00 UTC on 2026-08-16 every hour bills at the flat card of 2026-08-02, and from that instant billing is peak/off-peak on a new, higher card — peak hours -01:00-04:00 and 06:00-10:00 UTC daily at twice the off-peak rate. +01:00-04:00 and 06:00-10:00 UTC on weekdays at twice the off-peak +rate. Since 2026-08-22 weekends bill off-peak all day, on the Beijing +calendar, so peak is 35 hours a week rather than 49. This command reads no network and spends nothing: the schedule is the same one the cost estimates use, so what it prints is what the usage @@ -90,7 +92,7 @@ func pricingAt(now time.Time) *pricingResult { res.PeakWindowsUTC = append(res.PeakWindowsUTC, fmt.Sprintf("%s-%s", fmtMinutes(w.Start), fmtMinutes(w.End))) } - for _, m := range []string{deepseek.ModelFlash, deepseek.ModelPro} { + for _, m := range deepseek.Models { if p, ok := deepseek.PriceAt(m, now); ok { res.Current[m] = pricingPrice{p.CacheHitInput, p.CacheMissInput, p.Output} } @@ -133,7 +135,7 @@ func formatPricing(now time.Time) string { fmt.Fprintf(&b, "\nUSD per 1M tokens, in effect now (%s):\n", period.Label) w := tabwriter.NewWriter(&b, 0, 0, 2, ' ', 0) fmt.Fprintln(w, "MODEL\tIN (CACHED)\tIN (MISS)\tOUT") - for _, m := range []string{deepseek.ModelFlash, deepseek.ModelPro} { + for _, m := range deepseek.Models { if p, ok := deepseek.PriceAt(m, now); ok { fmt.Fprintf(w, "%s\t$%g\t$%g\t$%g\n", m, p.CacheHitInput, p.CacheMissInput, p.Output) } @@ -145,15 +147,15 @@ func formatPricing(now time.Time) string { windows = append(windows, fmtMinutes(win.Start)+"-"+fmtMinutes(win.End)) } if now.Before(deepseek.RepriceAt) { - fmt.Fprintf(&b, "\nfrom %s — peak hours %s UTC daily, all other hours off-peak at half of peak:\n", + fmt.Fprintf(&b, "\nfrom %s — peak hours %s UTC, Mon-Fri; all other hours, and all weekend, off-peak at half of peak:\n", deepseek.RepriceAt.Format("2006-01-02 15:04 UTC"), strings.Join(windows, " and ")) } else { - fmt.Fprintf(&b, "\nthe full card — peak hours %s UTC daily, all other hours off-peak at half of peak:\n", + fmt.Fprintf(&b, "\nthe full card — peak hours %s UTC, Mon-Fri; all other hours, and all weekend, off-peak at half of peak:\n", strings.Join(windows, " and ")) } w = tabwriter.NewWriter(&b, 0, 0, 2, ' ', 0) fmt.Fprintln(w, "MODEL\tPERIOD\tIN (CACHED)\tIN (MISS)\tOUT") - for _, m := range []string{deepseek.ModelFlash, deepseek.ModelPro} { + for _, m := range deepseek.Models { p, ok := deepseek.PriceAt(m, deepseek.RepriceAt) if !ok { continue diff --git a/internal/deepseek/client.go b/internal/deepseek/client.go index f23bfa9..75b8214 100644 --- a/internal/deepseek/client.go +++ b/internal/deepseek/client.go @@ -30,6 +30,10 @@ const DefaultBaseURL = "https://api.deepseek.com" const ( ModelFlash = "deepseek-v4-flash" ModelPro = "deepseek-v4-pro" + // ModelFlashVision is the experimental multimodal variant released + // 2026-08-21. It takes image input and bills at exactly the Flash + // rates; it does not support FIM completion. + ModelFlashVision = "deepseek-v4-flash-vision-exp" ) // Client talks to one DeepSeek deployment. The zero value is not usable; diff --git a/internal/deepseek/pricing.go b/internal/deepseek/pricing.go index c162ed0..9e1c690 100644 --- a/internal/deepseek/pricing.go +++ b/internal/deepseek/pricing.go @@ -53,6 +53,29 @@ type Price struct { // Source: https://api-docs.deepseek.com/quick_start/pricing (2026-08-13). var RepriceAt = time.Date(2026, time.August, 16, 16, 0, 0, 0, time.UTC) +// WeekendOffPeakAt is when weekends stopped billing peak at all: 16:00 +// UTC on 2026-08-22 (00:00 Beijing, Sunday 2026-08-23). From that +// instant a Saturday or Sunday *on the Beijing calendar* is off-peak for +// all 24 hours, so peak is 35 hours a week rather than 49. +// +// DeepSeek put this only in the pricing-page footnote, and only for the +// few days before it took effect — there is no changelog entry, and the +// live page now carries just the settled rule. The announcement survives +// at web.archive.org/web/20260822141620/https://api-docs.deepseek.com/quick_start/pricing/ +// +// "Effective 00:00 (Beijing Time) on Sunday, August 23, 2026, we will +// adjust our peak/off-peak billing rules, with off-peak rates applying +// throughout the day on weekends (Saturdays and Sundays, Beijing Time)." +// +// Gated on its own instant rather than folded into RepriceAt because the +// ledger reprices history: a call made in a peak window on Sunday +// 2026-08-17 or Saturday 2026-08-22 really did bill peak. +var WeekendOffPeakAt = time.Date(2026, time.August, 22, 16, 0, 0, 0, time.UTC) + +// beijing is the vendor's clock. China has observed no daylight saving +// since 1991, so a fixed offset is exact and needs no tzdata. +var beijing = time.FixedZone("CST", 8*60*60) + // pricesFlat is the card published 2026-08-02, in force before RepriceAt. // That instant has passed, so nothing live prices against it any more; it // stays because the ledger stores token counts rather than dollars, and a @@ -68,19 +91,26 @@ var pricesFlat = map[string]Price{ // publishes the peak figures rather than the rule, and they are exactly // double, so the multiplier is data, not interpretation. var pricesOffPeak = map[string]Price{ - ModelFlash: {CacheHitInput: 0.007, CacheMissInput: 0.22, Output: 0.66}, - ModelPro: {CacheHitInput: 0.022, CacheMissInput: 0.66, Output: 1.98}, + ModelFlash: {CacheHitInput: 0.007, CacheMissInput: 0.22, Output: 0.66}, + ModelFlashVision: {CacheHitInput: 0.007, CacheMissInput: 0.22, Output: 0.66}, + ModelPro: {CacheHitInput: 0.022, CacheMissInput: 0.66, Output: 1.98}, } +// Models are the priced models, in the order the rate card lists them. +// One list so a new model reaches every table at once: the vision variant +// was published upstream and absent here, which meters it at zero. +var Models = []string{ModelFlash, ModelFlashVision, ModelPro} + // PeakMultiplier scales the off-peak card during PeakWindows. const PeakMultiplier = 2.0 -// Window is a daily time-of-day window in minutes of the UTC day, end +// Window is a time-of-day window in minutes of the UTC day, end // exclusive. Upstream defines the boundaries in UTC, not Beijing. type Window struct{ Start, End int } -// PeakWindows are the daily peak hours from RepriceAt on: 01:00-04:00 -// and 06:00-10:00 UTC (09:00-12:00 and 14:00-18:00 Beijing). +// PeakWindows are the peak hours from RepriceAt on: 01:00-04:00 and +// 06:00-10:00 UTC (09:00-12:00 and 14:00-18:00 Beijing). Daily until +// WeekendOffPeakAt, weekdays only after it — see isBeijingWeekend. var PeakWindows = []Window{{Start: 1 * 60, End: 4 * 60}, {Start: 6 * 60, End: 10 * 60}} // Period names the pricing period one instant falls in. @@ -103,6 +133,9 @@ func PeriodAt(t time.Time) Period { } func inPeak(t time.Time) bool { + if !t.Before(WeekendOffPeakAt) && isBeijingWeekend(t) { + return false + } u := t.UTC() m := u.Hour()*60 + u.Minute() for _, w := range PeakWindows { @@ -113,6 +146,18 @@ func inPeak(t time.Time) bool { return false } +// isBeijingWeekend reports whether an instant falls on a Saturday or +// Sunday in Beijing. The weekday must be read on the vendor's clock +// because that is how the rule is published, which also means the +// weekend turns over at 16:00 UTC and not at midnight UTC. +func isBeijingWeekend(t time.Time) bool { + switch t.In(beijing).Weekday() { + case time.Saturday, time.Sunday: + return true + } + return false +} + // NextChange is the next instant after t at which the price of a call // changes: the repricing instant while the flat card is in force, then // the nearest peak-window boundary of the UTC day. @@ -120,18 +165,21 @@ func NextChange(t time.Time) time.Time { if t.Before(RepriceAt) { return RepriceAt } + // Every boundary this schedule has — a window edge, the weekend + // turnover at 16:00 UTC, and the policy start dates — lands on a UTC + // hour, so walking hours finds the next change exactly. Scanning a + // week of them covers the longest run of one period, Friday 10:00 + // UTC to Monday 01:00 UTC, with room to spare. u := t.UTC() - m := u.Hour()*60 + u.Minute() - day := time.Date(u.Year(), u.Month(), u.Day(), 0, 0, 0, 0, time.UTC) - for _, w := range PeakWindows { - if m < w.Start { - return day.Add(time.Duration(w.Start) * time.Minute) - } - if m < w.End { - return day.Add(time.Duration(w.End) * time.Minute) + here := PeriodAt(u).Label + at := u.Truncate(time.Hour).Add(time.Hour) + for i := 0; i < 8*24; i++ { + if PeriodAt(at).Label != here { + return at } + at = at.Add(time.Hour) } - return day.Add(24*time.Hour + time.Duration(PeakWindows[0].Start)*time.Minute) + return at } // PriceAt returns the effective rate card for a model at one instant: diff --git a/internal/deepseek/usage_test.go b/internal/deepseek/usage_test.go index c1f2c19..5b72bf7 100644 --- a/internal/deepseek/usage_test.go +++ b/internal/deepseek/usage_test.go @@ -217,6 +217,97 @@ func TestNextChange(t *testing.T) { } } +func TestWeekendsBillOffPeakAllDay(t *testing.T) { + // 02:00 UTC is inside the first published window, so before the rule + // every one of these billed peak. + cases := []struct { + day time.Time + want string + why string + }{ + {time.Date(2026, 8, 28, 2, 0, 0, 0, time.UTC), "peak", "Friday"}, + {time.Date(2026, 8, 29, 2, 0, 0, 0, time.UTC), "off-peak", "Saturday"}, + {time.Date(2026, 8, 30, 2, 0, 0, 0, time.UTC), "off-peak", "Sunday"}, + {time.Date(2026, 8, 31, 2, 0, 0, 0, time.UTC), "peak", "Monday"}, + } + for _, c := range cases { + if got := PeriodAt(c.day); got.Label != c.want { + t.Errorf("%s %v: got %q, want %q", c.why, c.day, got.Label, c.want) + } + } +} + +func TestWeekendTurnsOverOnTheVendorClock(t *testing.T) { + // The two instants that discriminate. Both published windows close at + // 10:00 UTC, well before 16:00 UTC where a UTC date and a Beijing date + // start to disagree — so reading the weekday in UTC instead of Beijing + // labels all 168 hours of the week identically, and every test written + // against the windows passes either way. Only these catch it. + friUTCsatBeijing := time.Date(2026, 8, 28, 16, 30, 0, 0, time.UTC) + if !isBeijingWeekend(friUTCsatBeijing) { + t.Error("Friday 16:30 UTC is already Saturday in Beijing — must count as weekend") + } + if isBeijingWeekend(friUTCsatBeijing.Add(-time.Hour)) { + t.Error("Friday 15:30 UTC is still Friday in Beijing — must not count as weekend") + } + sunUTCmonBeijing := time.Date(2026, 8, 30, 16, 30, 0, 0, time.UTC) + if isBeijingWeekend(sunUTCmonBeijing) { + t.Error("Sunday 16:30 UTC is already Monday in Beijing — must not count as weekend") + } +} + +func TestWeekendRuleDoesNotRepriceHistory(t *testing.T) { + // Exactly one weekend day ever billed peak, and this pins it. + // + // Time-of-use started 2026-08-16 16:00 UTC and the weekend rule + // 2026-08-22 16:00 UTC, six days apart. Both peak windows close at + // 10:00 UTC, before either boundary's 16:00, so: + // + // Sun 08-16 windows had already passed when time-of-use began -> flat + // Sat 08-22 time-of-use live, weekend rule not yet -> PEAK + // Sun 08-23 weekend rule live from 00:00 Beijing -> off-peak + // + // Saturday 2026-08-22 is the whole of the history that a naive + // "weekends were always off-peak" would wrongly refund. + sat := time.Date(2026, 8, 22, 2, 0, 0, 0, time.UTC) + if got := PeriodAt(sat).Label; got != "peak" { + t.Errorf("Sat 2026-08-22 predates the weekend rule: got %q, want peak", got) + } + sun := time.Date(2026, 8, 16, 2, 0, 0, 0, time.UTC) + if got := PeriodAt(sun).Label; got != "flat" { + t.Errorf("Sun 2026-08-16 predates time-of-use itself: got %q, want flat", got) + } + first := time.Date(2026, 8, 23, 2, 0, 0, 0, time.UTC) + if got := PeriodAt(first).Label; got != "off-peak" { + t.Errorf("Sun 2026-08-23, first weekend under the rule: got %q, want off-peak", got) + } +} + +func TestNextChangeCrossesTheWeekend(t *testing.T) { + // Friday 23:00 UTC used to promise peak at Saturday 01:00 UTC — a + // countdown to a flip that will not happen. The real next peak is + // Monday 01:00 UTC. + fri := time.Date(2026, 8, 28, 23, 0, 0, 0, time.UTC) + want := time.Date(2026, 8, 31, 1, 0, 0, 0, time.UTC) + if got := NextChange(fri); !got.Equal(want) { + t.Errorf("NextChange(Fri 23:00 UTC) = %v, want %v", got, want) + } +} + +func TestVisionModelIsPricedAtFlashRates(t *testing.T) { + // Released 2026-08-21, priced identically to Flash on every bucket in + // both published currencies. Absent from the card it would meter at zero. + at := time.Date(2026, 8, 26, 12, 0, 0, 0, time.UTC) // Wednesday, off-peak + vision, ok := PriceAt(ModelFlashVision, at) + if !ok { + t.Fatal("deepseek-v4-flash-vision-exp has no published rate") + } + flash, _ := PriceAt(ModelFlash, at) + if vision != flash { + t.Errorf("vision %+v != flash %+v", vision, flash) + } +} + func TestCacheSavings(t *testing.T) { // What the cached tokens would have cost at the miss rate, minus what // they did cost, under the card in force now. The exact figure moves diff --git a/internal/docs/corpus.tar.gz b/internal/docs/corpus.tar.gz index b6560f2..adeccc2 100644 Binary files a/internal/docs/corpus.tar.gz and b/internal/docs/corpus.tar.gz differ diff --git a/site/build.py b/site/build.py index e69e8bb..40887d2 100644 --- a/site/build.py +++ b/site/build.py @@ -100,6 +100,22 @@ def _release() -> str: # estimates and `deepseek pricing`. REPRICE_AT = "2026-08-16T16:00:00Z" +# A row may also carry `days`, the Beijing weekdays it applies on (0 is +# Sunday, as getUTCDay numbers them). That is how the weekend rule of +# 2026-08-22 is expressed: the new era repeats the same two windows but +# restricted to Mon-Fri, so on a Beijing Saturday or Sunday no windowed +# row matches and the era's windowless off-peak row answers all day. +# +# The weekday is read on the vendor's clock because that is how DeepSeek +# published it -- "Saturdays and Sundays, Beijing Time" -- which also puts +# the turnover at 16:00 UTC rather than midnight UTC. DeepSeek announced +# this only in the pricing-page footnote and only until it took effect; +# the live page now carries just the settled rule, so the announcement +# survives at +# https://web.archive.org/web/20260822141620/https://api-docs.deepseek.com/quick_start/pricing/ +WEEKEND_OFFPEAK_AT = "2026-08-22T16:00:00Z" +WEEKDAYS = [1, 2, 3, 4, 5] + PRICE_SCHEDULE = [ dict(label="flat", start=None, end=None, multiplier=1.0, effective="2026-08-02T00:00:00Z"), @@ -109,6 +125,12 @@ def _release() -> str: effective=REPRICE_AT), dict(label="peak", start=360, end=600, multiplier=2.0, effective=REPRICE_AT), + dict(label="off-peak", start=None, end=None, multiplier=1.0, + effective=WEEKEND_OFFPEAK_AT), + dict(label="peak", start=60, end=240, multiplier=2.0, days=WEEKDAYS, + effective=WEEKEND_OFFPEAK_AT), + dict(label="peak", start=360, end=600, multiplier=2.0, days=WEEKDAYS, + effective=WEEKEND_OFFPEAK_AT), ] @@ -122,9 +144,12 @@ def price_schedule_json(): for r in PRICE_SCHEDULE: start = "null" if r["start"] is None else str(r["start"]) end = "null" if r["end"] is None else str(r["end"]) + days = "" if r.get("days") is None else ( + ',"days":[%s]' % ",".join(str(d) for d in r["days"])) rows.append( - '{"label":%s,"start":%s,"end":%s,"multiplier":%s,"effective":%s}' - % (jstr(r["label"]), start, end, repr(r["multiplier"]), jstr(r["effective"])) + '{"label":%s,"start":%s,"end":%s,"multiplier":%s%s,"effective":%s}' + % (jstr(r["label"]), start, end, repr(r["multiplier"]), days, + jstr(r["effective"])) ) return "[" + ",".join(rows) + "]" @@ -143,9 +168,10 @@ def price_schedule_rows(): window = "all other hours" if windowed_peers else "all hours" else: window = f"{_fmt_minutes(r['start'])}–{_fmt_minutes(r['end'])} UTC" + days = "every day" if r.get("days") is None else "Mon–Fri" eff = r["effective"].replace("T", " ").replace(":00Z", " UTC") out.append( - f"{r['label']}{window}" + f"{r['label']}{window}{days}" f"{r['multiplier']:g}×" f"{eff}" ) @@ -168,18 +194,26 @@ def price_now_verdict(): "tiers are in effect, and the card below is the price." ) flip = eras[-1].replace("T", " ").replace(":00Z", " UTC") - windows = " and ".join( - f"{_fmt_minutes(r['start'])}–{_fmt_minutes(r['end'])}" - for r in PRICE_SCHEDULE if r["start"] is not None - ) + # Deduped: successive eras repeat the same two windows -- the weekend + # era changes which days they apply on, not which hours -- and without + # this the sentence lists each window once per era. + spans = [] + for r in PRICE_SCHEDULE: + if r["start"] is None: + continue + span = f"{_fmt_minutes(r['start'])}–{_fmt_minutes(r['end'])}" + if span not in spans: + spans.append(span) + windows = " and ".join(spans) live = _now() >= datetime.datetime.strptime( eras[-1], "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=datetime.timezone.utc) if live: return ( "Time-of-day billing is live. Peak hours are " - f"{windows} UTC daily, at twice the off-peak rate; every other " - "hour is off-peak. With JavaScript on, this strip reads your " - "clock and names the period you are in right now." + f"{windows} UTC, Monday to Friday, at twice the off-peak rate; " + "every other hour, and the whole weekend, is off-peak. With " + "JavaScript on, this strip reads your clock and names the " + "period you are in right now." ) return ( f"Before {flip}, every hour bills at the flat card " @@ -1195,6 +1229,8 @@ def jstr(s): deepseek-v4-flashoff-peak$0.007$0.22$0.66 peak$0.014$0.44$1.32 +deepseek-v4-flash-vision-expoff-peak$0.007$0.22$0.66 +peak$0.014$0.44$1.32 deepseek-v4-prooff-peak$0.022$0.66$1.98 peak$0.044$1.32$3.96 @@ -1334,13 +1370,13 @@ def jstr(s): slug="pricing/", crumb="pricing", title="DeepSeek API pricing: the schedule, the peak hours, and the period right now", - description="DeepSeek has billed peak/off-peak since 16:00 UTC on 2026-08-16: peak hours 01:00-04:00 and 06:00-10:00 UTC at twice the off-peak rate. The full schedule, the numbers per model in both periods, the flat card it replaced, and a strip that reads your clock and names the billing period you are in right now.", - keywords="deepseek pricing, deepseek api pricing, deepseek price increase 2026, deepseek repricing, deepseek peak hours, deepseek off-peak pricing, deepseek peak off-peak billing, deepseek api cost, deepseek v4 flash price, deepseek v4 pro price, deepseek pricing 2026-08-16, deepseek new rate card, deepseek token price", + description="DeepSeek has billed peak/off-peak since 16:00 UTC on 2026-08-16: peak hours 01:00-04:00 and 06:00-10:00 UTC, Monday to Friday, at twice the off-peak rate, with weekends off-peak all day since 2026-08-22. The full schedule, the numbers per model in both periods, the flat card it replaced, and a strip that reads your clock and names the billing period you are in right now.", + keywords="deepseek pricing, deepseek api pricing, deepseek price increase 2026, deepseek repricing, deepseek peak hours, deepseek off-peak pricing, deepseek peak off-peak billing, deepseek api cost, deepseek v4 flash price, deepseek v4 pro price, deepseek pricing 2026-08-16, deepseek new rate card, deepseek token price, deepseek weekend off-peak, deepseek peak weekdays only, deepseek v4 flash vision exp price", jsonld=faq([ ("What does the DeepSeek API cost right now?", - "It depends on the hour. Per 1M tokens (cache hit / cache miss / output): deepseek-v4-flash is $0.007 / $0.22 / $0.66 off-peak and $0.014 / $0.44 / $1.32 peak; deepseek-v4-pro is $0.022 / $0.66 / $1.98 off-peak and $0.044 / $1.32 / $3.96 peak. Peak hours are 01:00-04:00 and 06:00-10:00 UTC daily; every other hour is off-peak at half the peak rate."), + "It depends on the hour and the day. Per 1M tokens (cache hit / cache miss / output): deepseek-v4-flash is $0.007 / $0.22 / $0.66 off-peak and $0.014 / $0.44 / $1.32 peak; deepseek-v4-pro is $0.022 / $0.66 / $1.98 off-peak and $0.044 / $1.32 / $3.96 peak. Peak hours are 01:00-04:00 and 06:00-10:00 UTC, Monday to Friday; every other hour, and the whole weekend, is off-peak at half the peak rate. deepseek-v4-flash-vision-exp bills at exactly the deepseek-v4-flash rates."), ("What are DeepSeek's peak hours?", - "01:00-04:00 and 06:00-10:00 UTC, daily. The boundaries are defined in UTC; in Beijing time (UTC+8) they read 09:00-12:00 and 14:00-18:00. Every other hour is off-peak, at half the peak rate."), + "01:00-04:00 and 06:00-10:00 UTC, Monday to Friday. The boundaries are defined in UTC; in Beijing time (UTC+8) they read 09:00-12:00 and 14:00-18:00. Every other hour is off-peak, at half the peak rate. Since 16:00 UTC on 2026-08-22 weekends bill off-peak all day, on the Beijing calendar, so the weekend starts at 16:00 UTC on Friday and peak is 35 hours a week rather than 49."), ("When did DeepSeek's peak/off-peak pricing start?", "At 16:00 UTC on August 16, 2026, which is midnight in Beijing. Before that instant every hour billed at the flat card published 2026-08-02; from it, DeepSeek bills peak/off-peak on a new, higher card."), ("How much did DeepSeek raise its API prices?", @@ -1353,7 +1389,8 @@ def jstr(s): body="""

Pricing

Since 16:00 UTC on 2026-08-16, what a DeepSeek token costs -depends on the hour you spend it in. This page carries the schedule, both +depends on the hour you spend it in – and, since 2026-08-22, on the +day of the week too. This page carries the schedule, both rate cards, and a strip that reads your clock – the same data the CLI's estimates switch on, so the page and ds pricing can never disagree.

@@ -1366,12 +1403,12 @@ def jstr(s):

The schedule

One table drives everything on this page: each row is a billing period, -its daily window in UTC minutes, the multiplier on that era's base card, -and the instant the row takes effect. The strip above, the CLI's +its window in UTC minutes, the days it applies on, the multiplier on that +era's base card, and the instant the row takes effect. The strip above, the CLI's estimates and ds pricing all read the same rows.

- + """ + price_schedule_rows() + """ @@ -1381,6 +1418,17 @@ def jstr(s): (UTC+8) the peak hours read 09:00–12:00 and 14:00–18:00, which is the Chinese working day. The off-peak window covers the whole European and American working day, so batch work that can move west should.

+

Weekends have billed off-peak all day since 2026-08-22 16:00 +UTC (00:00 Beijing, Sunday 23 August). The weekend is the +Beijing Saturday and Sunday, so it turns over at 16:00 UTC, not +at midnight UTC – a Friday evening in Europe is already Saturday +upstream. That takes peak from 49 hours a week to 35, and it is the one +part of this schedule DeepSeek never put in its changelog: the +announcement sat in a footnote for a few days and the live page now +carries only the settled rule. It survives +in +the Internet Archive. If you are holding batch work for a cheap hour, +hold it for Saturday instead.

The card

USD per 1M tokens, in force since 2026-08-16 16:00 UTC. Off-peak is half @@ -1668,6 +1716,52 @@ def jstr(s): live API where that is possible; the in-terminal feed is ds docs changelog.

+

2026-08-22 · weekends are off-peak, all daynot in the changelog

+

From 16:00 UTC on 2026-08-22 – 00:00 Beijing on +Sunday 23 August – a Saturday or Sunday bills at the off-peak card for +all 24 hours. Peak drops from 49 hours a week to 35.

+

The weekend is the Beijing Saturday and Sunday, which +matters more than it sounds: the weekend turns over at 16:00 UTC, so a Friday +evening in Europe or a Friday morning in California is already Saturday +upstream, and already cheap.

+

This is the first pricing change DeepSeek has made without a changelog +entry. It appeared in the footnote of the +Models & Pricing +page for a few days before it took effect, and the live page now carries only +the settled rule – so the announcement itself exists nowhere on +api-docs.deepseek.com today. Verbatim, from the +archived +copy of 2026-08-22 14:16 UTC:

+

Effective 00:00 (Beijing Time) on Sunday, August 23, 2026, we +will adjust our peak/off-peak billing rules, with off-peak rates applying +throughout the day on weekends (Saturdays and Sundays, Beijing Time).

+

Worth stating plainly because a cost estimator that reads only the hour is +now wrong by 2× for 14 hours of every week, and reports +peak while the account is being charged half. If you vendored a 24-hour +schedule from anywhere – including from us – it needs a day axis. +ds pricing and this site were both fixed on 2026-08-24.

+

The rate card itself did not move. Only the clock did.

+ +

2026-08-21 · deepseek-v4-flash-vision-expexperimental

+

An experimental multimodal variant, reachable by setting +model=deepseek-v4-flash-vision-exp. It takes image input, and +images are converted to tokens by their dimensions and billed as ordinary +input tokens alongside your text.

+

It bills at exactly the deepseek-v4-flash rates, in both +currencies and every bucket: $0.007 / $0.22 / $0.66 off-peak and +$0.014 / $0.44 / $1.32 at peak, per 1M tokens. Same 1M context, same 384K max +output, same 2500 concurrency as flash. The one capability it drops is +FIM completion, which flash and pro both support in +non-thinking mode.

+

DeepSeek reports it as on par with flash on pure text and a large jump on +agent benchmarks that need vision – Chartography 64.3, ZeroBench +(pass@5) 35.0, DSBench-Hard 63.6 – putting its multimodal agent +ability, in their framing, close to Opus-4.8. Those are the vendor's numbers, +not ours; we have not run them.

+

Because it shipped after the 2026-08-16 switchover it has no flat +card, so there is nothing to reprice for it before that date. It is in +ds pricing from v0.5.1 on.

+

2026-08-16 · the repricing is liveconfirmed against a bill

It landed on schedule. At 16:00 UTC on 2026-08-16 – midnight in Beijing – DeepSeek's peak/off-peak card took effect, and @@ -1688,9 +1782,11 @@ def jstr(s):

PeriodDaily windowMultiplierEffective from
PeriodWindowDaysMultiplierEffective from

Cells read off-peak / peak. Peak hours are -01:00–04:00 and 06:00–10:00 UTC daily – 09:00–12:00 +01:00–04:00 and 06:00–10:00 UTC – 09:00–12:00 and 14:00–18:00 Beijing, seven hours a day – and every other hour -is off-peak at half the peak rate. In RMB, pro is +is off-peak at half the peak rate. Peak ran seven days a week until +2026-08-22; see weekends are off-peak +below. In RMB, pro is ¥0.15 / ¥4.5 / ¥13.5 off-peak and ¥0.3 / ¥9 / ¥27 at peak.

Against the flat card of 2026-08-02, off-peak / peak: diff --git a/site/cost/index.html b/site/cost/index.html index 3e76dee..33b41bd 100644 --- a/site/cost/index.html +++ b/site/cost/index.html @@ -102,6 +102,8 @@

The rate card

deepseek-v4-flashoff-peak$0.007$0.22$0.66 peak$0.014$0.44$1.32 +deepseek-v4-flash-vision-expoff-peak$0.007$0.22$0.66 +peak$0.014$0.44$1.32 deepseek-v4-prooff-peak$0.022$0.66$1.98 peak$0.044$1.32$3.96 diff --git a/site/llms.txt b/site/llms.txt index 14b2580..43ea90a 100644 --- a/site/llms.txt +++ b/site/llms.txt @@ -24,20 +24,20 @@ response body unmodified. Exit codes: 0 ok, 1 error, 2 auth, 3 no balance, - [Commands](https://thevibeworks.github.io/deepseek-cli/commands/): every command and flag: chat (with --interactive), anthropic, respond, fim, tokens, docs, models, balance, pricing, usage, session, status, check, raw, plus global flags and exit codes - [Formats](https://thevibeworks.github.io/deepseek-cli/formats/): DeepSeek's four wire formats compared: auth, thinking controls, tool support, Claude model remapping, and the token-accounting convention that differs between them - [Cost](https://thevibeworks.github.io/deepseek-cli/cost/): the rate card, what the context cache is worth, the thinking surcharge and how it varies by effort level, the local usage ledger, and the caveats on every figure -- [Pricing](https://thevibeworks.github.io/deepseek-cli/pricing/): the dated schedule for DeepSeek's move to peak/off-peak billing at 16:00 UTC on 2026-08-16 (peak 01:00-04:00 and 06:00-10:00 UTC at twice the off-peak rate), the current flat card, the new per-model numbers, and a strip that names the billing period in effect right now +- [Pricing](https://thevibeworks.github.io/deepseek-cli/pricing/): the dated schedule for DeepSeek's move to peak/off-peak billing at 16:00 UTC on 2026-08-16 (peak 01:00-04:00 and 06:00-10:00 UTC, Mon-Fri, at twice the off-peak rate; weekends off-peak all day since 2026-08-22), the retired flat card, the per-model numbers, and a strip that names the billing period in effect right now - [Benchmarks](https://thevibeworks.github.io/deepseek-cli/bench/): how deepseek-v4-pro (GA 0813) and v4-flash score against Kimi K3, GLM-5.2, Claude Opus 4.8 and Fable 5 on the agent suites (DeepSeek's own launch chart, with caveats), what the GA checkpoint changed over the preview, and the DeepSeek kill line (斩杀线) economics idea with the price gap to GPT-5.6 -- [News](https://thevibeworks.github.io/deepseek-cli/news/): what is changing around the DeepSeek API: the repricing dated 2026-08-16 (peak/off-peak billing on a new card), DeepSeek shipping dsh (DeepSeek Harness, its official agent harness) and how it relates to this CLI, V4-Pro's GA release, V4-Flash's official release, and what each does to the cost of a call +- [News](https://thevibeworks.github.io/deepseek-cli/news/): what is changing around the DeepSeek API: weekends going off-peak all day on 2026-08-22 (announced only in a page footnote, never in the changelog), the deepseek-v4-flash-vision-exp release of 2026-08-21, the repricing dated 2026-08-16 (peak/off-peak billing on a new card), DeepSeek shipping dsh (DeepSeek Harness, its official agent harness) and how it relates to this CLI, V4-Pro's GA release, V4-Flash's official release, and what each does to the cost of a call - [Agents](https://thevibeworks.github.io/deepseek-cli/agents/): the scripting contract: output streams, JSON response shapes per format, exit-code semantics, retry behaviour - [Playground](https://thevibeworks.github.io/deepseek-cli/playground/): the DeepSeek API in a browser with no API key: all four wire formats, streaming, and the equivalent deepseek command for every request ## Key facts - Endpoints: POST /chat/completions, POST /anthropic/v1/messages, POST /responses, POST /beta/completions (FIM), GET /models, GET /user/balance -- Models: deepseek-v4-flash and deepseek-v4-pro only. Claude model names sent to the Anthropic endpoint are remapped server-side (claude-opus* to pro, claude-sonnet*/claude-haiku* to flash, unrecognised to flash) +- Models: deepseek-v4-flash, deepseek-v4-pro, and deepseek-v4-flash-vision-exp (experimental, image input, priced exactly as flash, no FIM support). Claude model names sent to the Anthropic endpoint are remapped server-side (claude-opus* to pro, claude-sonnet*/claude-haiku* to flash, unrecognised to flash) - Token accounting differs by format: on /anthropic/v1/messages, usage.input_tokens EXCLUDES cache reads, so the full prompt is input_tokens + cache_read_input_tokens. The chat and Responses formats include cached tokens in their input count - Thinking mode is on by default and adds a fixed input-token template whose size depends on --effort, not on prompt length. Measured 2026-08-05 on flash: none/minimal/low +0, medium/high/xhigh +79, max +92. On pro: +0 except max, which is +79. So --effort low on flash removes the surcharge entirely and still reasons - A cached input token costs about 1/50th of an uncached one ($0.0028 vs $0.14 per 1M on flash); on the card of 2026-08-16 the ratio narrows to about 1/30th -- Repricing is dated: at 16:00 UTC on 2026-08-16 DeepSeek moves to peak/off-peak billing on a new, higher card. Peak hours 01:00-04:00 and 06:00-10:00 UTC daily at twice the off-peak rate. Per 1M tokens (cache hit/miss/output): flash $0.007/$0.22/$0.66 off-peak, $0.014/$0.44/$1.32 peak; pro $0.022/$0.66/$1.98 off-peak, $0.044/$1.32/$3.96 peak. `deepseek pricing` prints the schedule and the period in effect; cost estimates switch cards on the effective instant, never before +- Repricing is dated: at 16:00 UTC on 2026-08-16 DeepSeek moves to peak/off-peak billing on a new, higher card. Peak hours 01:00-04:00 and 06:00-10:00 UTC, Monday to Friday, at twice the off-peak rate. Since 16:00 UTC on 2026-08-22 weekends bill off-peak all day on the Beijing calendar, so the weekend turns over at 16:00 UTC and peak is 35 hours a week, not 49. Per 1M tokens (cache hit/miss/output): flash and flash-vision-exp $0.007/$0.22/$0.66 off-peak, $0.014/$0.44/$1.32 peak; pro $0.022/$0.66/$1.98 off-peak, $0.044/$1.32/$3.96 peak. `deepseek pricing` prints the schedule and the period in effect; cost estimates switch cards on the effective instant, never before - Text only: image, document and search-result content blocks are rejected in every format - Tool calls are printed, never executed - A free tier exists: `deepseek free` enrols with a proof-of-work puzzle instead of a signup, then relays through a gateway run by the project. Flash only, 30 requests and 20K output tokens per UTC day, 4K output per call. A pro request is refused rather than downgraded. Prompts transit the gateway; token counts and cost are recorded, prompts and completions are not diff --git a/site/news/index.html b/site/news/index.html index f4ecd58..3ac33d1 100644 --- a/site/news/index.html +++ b/site/news/index.html @@ -82,6 +82,52 @@

News

live API where that is possible; the in-terminal feed is ds docs changelog.

+

2026-08-22 · weekends are off-peak, all daynot in the changelog

+

From 16:00 UTC on 2026-08-22 – 00:00 Beijing on +Sunday 23 August – a Saturday or Sunday bills at the off-peak card for +all 24 hours. Peak drops from 49 hours a week to 35.

+

The weekend is the Beijing Saturday and Sunday, which +matters more than it sounds: the weekend turns over at 16:00 UTC, so a Friday +evening in Europe or a Friday morning in California is already Saturday +upstream, and already cheap.

+

This is the first pricing change DeepSeek has made without a changelog +entry. It appeared in the footnote of the +Models & Pricing +page for a few days before it took effect, and the live page now carries only +the settled rule – so the announcement itself exists nowhere on +api-docs.deepseek.com today. Verbatim, from the +archived +copy of 2026-08-22 14:16 UTC:

+

Effective 00:00 (Beijing Time) on Sunday, August 23, 2026, we +will adjust our peak/off-peak billing rules, with off-peak rates applying +throughout the day on weekends (Saturdays and Sundays, Beijing Time).

+

Worth stating plainly because a cost estimator that reads only the hour is +now wrong by 2× for 14 hours of every week, and reports +peak while the account is being charged half. If you vendored a 24-hour +schedule from anywhere – including from us – it needs a day axis. +ds pricing and this site were both fixed on 2026-08-24.

+

The rate card itself did not move. Only the clock did.

+ +

2026-08-21 · deepseek-v4-flash-vision-expexperimental

+

An experimental multimodal variant, reachable by setting +model=deepseek-v4-flash-vision-exp. It takes image input, and +images are converted to tokens by their dimensions and billed as ordinary +input tokens alongside your text.

+

It bills at exactly the deepseek-v4-flash rates, in both +currencies and every bucket: $0.007 / $0.22 / $0.66 off-peak and +$0.014 / $0.44 / $1.32 at peak, per 1M tokens. Same 1M context, same 384K max +output, same 2500 concurrency as flash. The one capability it drops is +FIM completion, which flash and pro both support in +non-thinking mode.

+

DeepSeek reports it as on par with flash on pure text and a large jump on +agent benchmarks that need vision – Chartography 64.3, ZeroBench +(pass@5) 35.0, DSBench-Hard 63.6 – putting its multimodal agent +ability, in their framing, close to Opus-4.8. Those are the vendor's numbers, +not ours; we have not run them.

+

Because it shipped after the 2026-08-16 switchover it has no flat +card, so there is nothing to reprice for it before that date. It is in +ds pricing from v0.5.1 on.

+

2026-08-16 · the repricing is liveconfirmed against a bill

It landed on schedule. At 16:00 UTC on 2026-08-16 – midnight in Beijing – DeepSeek's peak/off-peak card took effect, and @@ -102,9 +148,11 @@

2026-08-16 · the repricing is liveCells read off-peak / peak. Peak hours are -01:00–04:00 and 06:00–10:00 UTC daily – 09:00–12:00 +01:00–04:00 and 06:00–10:00 UTC – 09:00–12:00 and 14:00–18:00 Beijing, seven hours a day – and every other hour -is off-peak at half the peak rate. In RMB, pro is +is off-peak at half the peak rate. Peak ran seven days a week until +2026-08-22; see weekends are off-peak +below. In RMB, pro is ¥0.15 / ¥4.5 / ¥13.5 off-peak and ¥0.3 / ¥9 / ¥27 at peak.

Against the flat card of 2026-08-02, off-peak / peak: diff --git a/site/pricing.js b/site/pricing.js index 4748d98..0775f8a 100644 --- a/site/pricing.js +++ b/site/pricing.js @@ -13,6 +13,14 @@ // wins inside its [start, end) window, and the era's windowless row is // the answer everywhere else. Window starts are inclusive, ends // exclusive, so 04:00:00 UTC is already off-peak. +// +// A row may also carry `days`, the Beijing weekdays it applies on (0 is +// Sunday, as in getUTCDay). That is how the weekend rule of 2026-08-22 +// is expressed: the era's peak rows are restricted to Mon-Fri, so on a +// Saturday or Sunday no windowed row matches and the era's windowless +// off-peak row answers all day. The weekday is read on the vendor's +// clock because that is how the rule is published, which also means the +// weekend turns over at 16:00 UTC rather than at midnight UTC. (function (global) { 'use strict'; @@ -25,6 +33,22 @@ return date.getUTCHours() * 60 + date.getUTCMinutes(); } + // Beijing is UTC+8 and has observed no daylight saving since 1991, so + // a fixed shift is exact and needs no timezone database. + var BEIJING_OFFSET_MS = 8 * 3600000; + + // Whether a row applies on the Beijing weekday of this instant. A row + // without `days` applies on every day, which is what every pre-weekend + // era row is. + function onDay(row, date) { + if (!row.days) return true; + var weekday = new Date(date.getTime() + BEIJING_OFFSET_MS).getUTCDay(); + for (var i = 0; i < row.days.length; i++) { + if (row.days[i] === weekday) return true; + } + return false; + } + // The distinct effective instants, ascending, in ms. Each is an era. function eras(schedule) { var seen = {}; @@ -65,43 +89,58 @@ base = r; continue; } - if (m >= r.start && m < r.end) return r; + if (m >= r.start && m < r.end && onDay(r, date)) return r; } return base; } - // The next instant the answer above changes: the next era's effective - // instant, or the next window boundary of the current era, whichever - // comes first. Null only for a one-row schedule with no windows. + // The next instant the answer above changes. + // + // Every boundary this schedule has lands on a window edge, an era's + // effective instant, or the 16:00 UTC weekend turnover -- so the change + // is found by walking the candidate instants in order and returning the + // first whose period differs from the one running now. + // + // The search runs over eight days rather than two because peak is no + // longer daily: from Friday 10:00 UTC the next peak is Monday 01:00 + // UTC, 63 hours away, and a two-day horizon would have missed it and + // reported no change at all. Null only for a schedule that never + // changes. function nextChange(schedule, date) { var t = date.getTime(); - var all = eras(schedule); - var era = eraAt(schedule, date); + var here = periodFor(schedule, date); var candidates = []; + var all = eras(schedule); for (var i = 0; i < all.length; i++) { - if (all[i] > t) { - candidates.push(all[i]); - break; - } + if (all[i] > t) candidates.push(all[i]); } + var era = eraAt(schedule, date); var bounds = []; for (var j = 0; j < schedule.length; j++) { var r = schedule[j]; if (Date.parse(r.effective) !== era || r.start === null) continue; bounds.push(r.start, r.end); } - if (bounds.length) { - var midnight = Date.UTC( - date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()); - for (var d = 0; d <= 1; d++) { - for (var k = 0; k < bounds.length; k++) { - var at = midnight + d * 86400000 + bounds[k] * 60000; - if (at > t) candidates.push(at); - } + var midnight = Date.UTC( + date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()); + for (var d = 0; d <= 8; d++) { + for (var k = 0; k < bounds.length; k++) { + var at = midnight + d * 86400000 + bounds[k] * 60000; + if (at > t) candidates.push(at); + } + // The weekend turns over at 16:00 UTC, which is not a window edge. + var turn = midnight + d * 86400000 + 16 * 3600000; + if (turn > t) candidates.push(turn); + } + candidates.sort(function (a, b) { return a - b; }); + for (var n = 0; n < candidates.length; n++) { + var next = periodFor(schedule, new Date(candidates[n])); + if (!here || !next) continue; + if (next.label !== here.label || next.multiplier !== here.multiplier) { + return new Date(candidates[n]); } } - if (!candidates.length) return null; - return new Date(Math.min.apply(null, candidates)); + return null; } // A wait, the way a person reads one: days and hours far out, minutes @@ -192,6 +231,7 @@ utcMinute: utcMinute, eras: eras, eraAt: eraAt, + onDay: onDay, periodFor: periodFor, nextChange: nextChange, humanUntil: humanUntil, diff --git a/site/pricing.test.js b/site/pricing.test.js index a6a2369..37e2c6b 100644 --- a/site/pricing.test.js +++ b/site/pricing.test.js @@ -47,11 +47,16 @@ const schedule = JSON.parse(m[1]); // Upstream ground truth (api-docs.deepseek.com/quick_start/pricing, // 2026-08-13): flat until 16:00 UTC 2026-08-16, then peak/off-peak with // peak 01:00-04:00 and 06:00-10:00 UTC at twice the off-peak rate. +// Then, from 16:00 UTC 2026-08-22 (00:00 Beijing, Sun 23 Aug), peak is +// restricted to Beijing weekdays and weekends bill off-peak all day. const truth = [ { label: 'flat', start: null, end: null, multiplier: 1, effective: '2026-08-02T00:00:00Z' }, { label: 'off-peak', start: null, end: null, multiplier: 1, effective: '2026-08-16T16:00:00Z' }, { label: 'peak', start: 60, end: 240, multiplier: 2, effective: '2026-08-16T16:00:00Z' }, { label: 'peak', start: 360, end: 600, multiplier: 2, effective: '2026-08-16T16:00:00Z' }, + { label: 'off-peak', start: null, end: null, multiplier: 1, effective: '2026-08-22T16:00:00Z' }, + { label: 'peak', start: 60, end: 240, multiplier: 2, days: [1, 2, 3, 4, 5], effective: '2026-08-22T16:00:00Z' }, + { label: 'peak', start: 360, end: 600, multiplier: 2, days: [1, 2, 3, 4, 5], effective: '2026-08-22T16:00:00Z' }, ]; check('the embedded schedule is the upstream ground truth', JSON.stringify(schedule) === JSON.stringify(truth), @@ -126,6 +131,52 @@ check('between the windows the next change is the second start', check('after the last window the next change is tomorrow 01:00', nextAt(at(2026, 8, 17, 12, 0)) === '2026-08-18T01:00:00.000Z'); +// --------------------------------------------------------------------- +// The weekend rule, live since 16:00 UTC on 2026-08-22. +// +// 2026-08-28 is a Friday, 08-29 a Saturday, 08-30 a Sunday, 08-31 a Monday. + +const label = (d) => P.periodFor(schedule, d).label; + +check('a Saturday inside a published window still bills off-peak', + label(at(2026, 8, 29, 2, 0)) === 'off-peak'); +check('a Sunday inside a published window still bills off-peak', + label(at(2026, 8, 30, 2, 0)) === 'off-peak'); +check('a Friday in the same window bills peak', + label(at(2026, 8, 28, 2, 0)) === 'peak'); +check('a Monday in the same window bills peak', + label(at(2026, 8, 31, 2, 0)) === 'peak'); + +// The weekend is read on the vendor's clock, so it turns over at 16:00 +// UTC. Both published windows close at 10:00 UTC, well before the point +// where a UTC date and a Beijing date diverge -- so with today's windows +// no billed instant tells the two readings apart, and every test written +// against the schedule passes with the Beijing shift deleted. +// +// onDay is therefore pinned directly. It is the only place the shift is +// observable, and the day a window moves past 16:00 UTC it stops being a +// matter of taste and starts costing money. +const weekdays = { days: [1, 2, 3, 4, 5] }; +check('Friday 15:30 UTC is still Friday in Beijing', + P.onDay(weekdays, at(2026, 8, 28, 15, 30)) === true); +check('Friday 16:30 UTC is already Saturday in Beijing', + P.onDay(weekdays, at(2026, 8, 28, 16, 30)) === false); +check('Sunday 16:30 UTC is already Monday in Beijing', + P.onDay(weekdays, at(2026, 8, 30, 16, 30)) === true); +check('a row without days applies on any day', + P.onDay({}, at(2026, 8, 29, 2, 0)) === true); +check('the weekend rule does not reprice the Saturday before it', + label(at(2026, 8, 22, 2, 0)) === 'peak'); +check('the first Sunday under the rule bills off-peak', + label(at(2026, 8, 23, 2, 0)) === 'off-peak'); + +// A countdown must not promise a flip that will not happen. Before the +// rule, Friday 23:00 UTC pointed at Saturday 01:00 UTC. +check('from Friday night the next peak is Monday, not Saturday', + nextAt(at(2026, 8, 28, 23, 0)) === '2026-08-31T01:00:00.000Z'); +check('from Saturday the next peak is still Monday', + nextAt(at(2026, 8, 29, 12, 0)) === '2026-08-31T01:00:00.000Z'); + // --------------------------------------------------------------------- // The strip's sentence, at instants on both sides of the flip. diff --git a/site/pricing/index.html b/site/pricing/index.html index eaee0af..adc58c6 100644 --- a/site/pricing/index.html +++ b/site/pricing/index.html @@ -4,8 +4,8 @@ DeepSeek API pricing: the schedule, the peak hours, and the period right now - - + + @@ -13,7 +13,7 @@ - + @@ -21,7 +21,7 @@ - + @@ -30,7 +30,7 @@ +