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"
deepseek-v4-flashdeepseek-v4-flash-vision-expdeepseek-v4-proSince 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.
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.
| Period | Daily window | Multiplier | Effective from | |
|---|---|---|---|---|
| Period | Window | Days | Multiplier | Effective 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 @@
deepseek-v4-flashdeepseek-v4-flash-vision-expdeepseek-v4-prods docs 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.
+ +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.
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 @@
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 @@