Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 37 additions & 3 deletions gateway/internal/meter/meter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -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 {
Expand All @@ -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
Expand Down
63 changes: 63 additions & 0 deletions gateway/internal/meter/weekend_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
14 changes: 8 additions & 6 deletions internal/cli/pricing.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}
}
Expand Down Expand Up @@ -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)
}
Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions internal/deepseek/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
76 changes: 62 additions & 14 deletions internal/deepseek/pricing.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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 {
Expand All @@ -113,25 +146,40 @@ 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.
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:
Expand Down
Loading