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
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ jobs:
node site/playground.test.js
node site/playground.dom.test.js

# The pricing page embeds the schedule its strip reads; this pins
# the period arithmetic and the schedule against the ground truth.
- name: pricing page
run: node site/pricing.test.js

# The rate card exists in two modules that cannot import each other.
- name: rate cards have not drifted
run: make price-check
Expand Down
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,14 @@ deepseek models --json
deepseek balance --json # exits 3 if exhausted
deepseek usage --since 7d --json # local ledger, not billing
deepseek usage --entries --json # individual calls
deepseek pricing --json # the schedule and the billing period right now
```

Pricing is time-of-day from 2026-08-16 16:00 UTC: peak hours 01:00–04:00
and 06:00–10:00 UTC bill at twice the off-peak rate. `pricing` computes
the current period locally — no network, nothing spent — from the same
schedule the cost estimates use.

### Documentation, offline

The binary carries every page of api-docs.deepseek.com plus the FAQ.
Expand Down Expand Up @@ -198,6 +204,7 @@ These are computed locally, not from the API:

```
usage --json {"since","total":{...},"by_model":{...},"by_api":{...}}
pricing --json {"now_utc","now_local","now_beijing","period","multiplier","next_change","reprice_at","peak_windows_utc":[...],"peak_multiplier","current":{...},"off_peak":{...},"peak":{...},"source"}
check --json {"base_url","key_set","ok","probes":[{"name","path","ok","detail","error","ms"}]}
session ls --json [{"name","model","turns","updated","bytes"}]
status --json {"base_url","ok","models":[...],"latency_ms","balance","status_page"}
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ site-check:
node site/md.test.js
node site/playground.test.js
node site/playground.dom.test.js
node site/pricing.test.js
node site/waves.test.js
node site/waves.dom.test.js
./site/bans.sh
Expand Down
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ One command per endpoint, named for what it does.
| `tokens` | `POST /beta/completions` | Exact token counts, from the model's own tokenizer. |
| `docs` | *(local)* | DeepSeek's own API docs, in the binary. Search, read, and ask. |
| `usage` | *(local)* | What this CLI has spent, from its own ledger. |
| `pricing` | *(local)* | The rate card, the time-of-day schedule, and the billing period right now. |
| `session` | *(local)* | The conversations `chat --continue` replays. |
| `status` | `GET /models`, `/user/balance` | Is it up, for this key, from here. Costs nothing. |
| `check` | *(all six)* | Preflight. |
Expand Down Expand Up @@ -382,10 +383,13 @@ Honest limits:
- Costs are **estimates** from the published USD rate card, not billed
amounts. Token counts are exact, and they are what the ledger stores,
so old rows can be repriced when the card changes.
- DeepSeek has announced peak/off-peak pricing (2× during 09:00–12:00
and 14:00–18:00 Beijing time) with **no effective date**. It is
deliberately not applied — guessing that a call was billed double
would be inventing data.
- DeepSeek's repricing **is dated and encoded**: at 16:00 UTC on
2026-08-16 billing moves to peak/off-peak on a new, higher card (peak
01:00–04:00 and 06:00–10:00 UTC, 2× the off-peak rate). Estimates use
the flat card until that instant and switch on it automatically —
never before, because applying a price before its effective date
would be inventing data. `deepseek pricing` prints the schedule and
the period in effect right now.
- `--no-ledger` skips the write; `--no-stats` hides the line.

## For agents
Expand Down
8 changes: 8 additions & 0 deletions TASTE.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,14 @@ the multiplier goes when it lands.
**Expires.** When DeepSeek announces the effective date — then the
multiplier goes in, gated on that date.

**Expired 2026-08-13.** DeepSeek dated it: peak/off-peak billing on a new
card from 2026-08-16 16:00 UTC, windows defined in UTC (01:00–04:00 and
06:00–10:00). The multiplier went in exactly as this entry prescribed —
gated on the effective instant, never applied before it. The schedule
lives in `internal/deepseek/pricing.go`, mirrored by the gateway meter and
the site's pricing page; `deepseek pricing` prints it. The principle
stands for the next undated announcement.

---

## 2026-08-05 rejected: executing the tool calls the model asks for
Expand Down
148 changes: 132 additions & 16 deletions gateway/internal/meter/meter.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"bytes"
"encoding/json"
"strings"
"time"
)

// Usage is token accounting normalised across the four wire formats.
Expand Down Expand Up @@ -39,32 +40,98 @@ type Price struct {
Output float64
}

// Rates as published on 2026-08-02.
var rates = map[string]Price{
// RepriceAt is when DeepSeek's dated repricing takes effect: 16:00 UTC
// on 2026-08-16, announced 2026-08-13. From that instant billing is
// peak/off-peak on a new, higher card. Under-charging our own budget
// after the flip would drain the credit pool at yesterday's prices, so
// the switch is encoded here and gated on the date, exactly as the CLI's
// copy does.
var RepriceAt = time.Date(2026, time.August, 16, 16, 0, 0, 0, time.UTC)

// 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},
"deepseek-v4-pro": {CacheHitInput: 0.003625, CacheMissInput: 0.435, Output: 0.87},
}

// PriceFor returns the rate card for a model, defaulting to the more
// expensive one. Charging an unknown model at pro rates is deliberate:
// if DeepSeek ships a third model and we have not updated this table, we
// want to over-charge our own budget, not under-charge it.
// ratesOffPeak is the base card from RepriceAt on; during peakWindows
// 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},
}

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.
var peakWindows = [][2]int{{1 * 60, 4 * 60}, {6 * 60, 10 * 60}}

func inPeak(t time.Time) bool {
u := t.UTC()
m := u.Hour()*60 + u.Minute()
for _, w := range peakWindows {
if m >= w[0] && m < w[1] {
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
// this table, we want to over-charge our own budget, not under-charge it.
func PriceFor(model string) Price {
if p, ok := rates[model]; ok {
return p
return PriceAt(model, time.Now())
}

// PriceAt is PriceFor at a chosen instant: that era's base card, doubled
// inside a peak window.
func PriceAt(model string, t time.Time) Price {
if t.Before(RepriceAt) {
return cardFor(ratesFlat, model)
}
if strings.Contains(model, "pro") || model == "" {
return rates["deepseek-v4-pro"]
p := cardFor(ratesOffPeak, model)
if inPeak(t) {
p = scale(p, peakMultiplier)
}
if strings.Contains(model, "flash") {
return rates["deepseek-v4-flash"]
return p
}

func cardFor(cards map[string]Price, model string) Price {
if p, ok := cards[model]; ok {
return p
}
switch {
case strings.Contains(model, "pro") || model == "":
return cards["deepseek-v4-pro"]
case strings.Contains(model, "flash"):
return cards["deepseek-v4-flash"]
default:
return cards["deepseek-v4-pro"]
}
return rates["deepseek-v4-pro"]
}

// Cost prices a usage record.
func scale(p Price, mult float64) Price {
p.CacheHitInput *= mult
p.CacheMissInput *= mult
p.Output *= mult
return p
}

// Cost prices a usage record at the card in force right now — the
// response being settled just arrived.
func Cost(model string, u Usage) float64 {
p := PriceFor(model)
return CostAt(model, u, time.Now())
}

// CostAt prices a usage record under the card in force at one instant.
func CostAt(model string, u Usage, t time.Time) float64 {
return costWith(PriceAt(model, t), u)
}

func costWith(p Price, u Usage) float64 {
const perMillion = 1_000_000.0
miss := u.InputTokens - u.CacheHitTokens
if miss < 0 {
Expand Down Expand Up @@ -93,18 +160,67 @@ func Cost(model string, u Usage) float64 {
// A search request breaks the first rule: the pages DeepSeek reads on the
// caller's behalf arrive as input tokens the body never contained, so
// searchInputAllowance is added to the input bound instead.
// A third rule joined them with the dated repricing: the reservation is
// priced at the dearest card the request could settle under, not the
// card of the admission instant. A request admitted just before a peak
// window (or just before the repricing flip) can settle inside it, and
// an estimate the clock can outrun is not a ceiling.
func Estimate(model string, requestBytes, maxTokens int, search bool) float64 {
return EstimateAt(model, requestBytes, maxTokens, search, time.Now())
}

// EstimateAt is Estimate at a chosen instant.
func EstimateAt(model string, requestBytes, maxTokens int, search bool, t time.Time) float64 {
input := requestBytes + 1
if search {
input += searchInputAllowance
}
return Cost(model, Usage{
return costWith(ceilingAt(model, t), Usage{
InputTokens: input,
OutputTokens: maxTokens + reasoningAllowance,
Found: false,
})
}

// ceilingAt is the dearest card a request admitted at t could settle
// under. Upstream holds a connection up to ten minutes before inference
// begins, so a request is given an hour of in-flight allowance: if that
// hour crosses the repricing flip or touches a peak window, the
// reservation is priced at the dearer side. Off-peak admissions far from
// any boundary still reserve at the off-peak card — a ceiling should be
// unbeatable, not double.
func ceilingAt(model string, t time.Time) Price {
const inFlight = time.Hour
if t.Add(inFlight).Before(RepriceAt) {
return cardFor(ratesFlat, model)
}
if !t.Before(RepriceAt) && !peakTouches(t, inFlight) {
return cardFor(ratesOffPeak, model)
}
return scale(cardFor(ratesOffPeak, model), peakMultiplier)
}

// peakTouches reports whether any instant of [t, t+d] falls in a peak
// window. The endpoint checks cover every span shorter than the gaps
// between windows; the start-of-window check keeps this correct even if
// a future card ships a window shorter than the span.
func peakTouches(t time.Time, d time.Duration) bool {
if inPeak(t) || inPeak(t.Add(d)) {
return true
}
u := t.UTC()
m := u.Hour()*60 + u.Minute()
span := int(d / time.Minute)
for _, w := range peakWindows {
for _, start := range []int{w[0], w[0] + 24*60} {
if start > m && start < m+span {
return true
}
}
}
return false
}

// reasoningAllowance is the output headroom reserved for chain-of-thought
// tokens on top of the caller's visible max_tokens. 32k covers the
// longest thinking runs measured live; at flash rates it prices at under
Expand Down
49 changes: 49 additions & 0 deletions gateway/internal/meter/meter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"math"
"strings"
"testing"
"time"
)

// The payloads below are verbatim from the live API on 2026-08-05, one
Expand Down Expand Up @@ -183,6 +184,54 @@ func TestEstimateExceedsATypicalRealCharge(t *testing.T) {
}
}

// The dated repricing: flat until 16:00 UTC on 2026-08-16, then a new
// base card off-peak with peak windows at exactly double. Under-charging
// after the flip would drain the credit pool at yesterday's prices.
func TestRepricingSwitchesOnItsEffectiveInstant(t *testing.T) {
u := Usage{InputTokens: 1_000_000, OutputTokens: 1_000_000, Found: true}

before := CostAt("deepseek-v4-flash", u, RepriceAt.Add(-time.Second))
if math.Abs(before-0.42) > 1e-9 {
t.Errorf("before the flip: got %v, want the flat 0.42", before)
}
// 16:00 UTC is outside both peak windows: the off-peak card.
at := CostAt("deepseek-v4-flash", u, RepriceAt)
if math.Abs(at-0.88) > 1e-9 {
t.Errorf("at the flip: got %v, want the off-peak 0.88", at)
}
// 02:00 UTC is inside 01:00-04:00: double.
peak := CostAt("deepseek-v4-flash", u, time.Date(2026, 8, 17, 2, 0, 0, 0, time.UTC))
if math.Abs(peak-1.76) > 1e-9 {
t.Errorf("in a peak window: got %v, want 1.76", peak)
}
}

// The reservation must be a true upper bound across period boundaries: a
// request admitted minutes before a peak window (or before the flip) can
// settle inside it, so its ceiling is priced at the dearer side.
func TestEstimateCeilingCoversTheNextPeriod(t *testing.T) {
const model = "deepseek-v4-flash"
justBeforePeak := time.Date(2026, 8, 17, 5, 30, 0, 0, time.UTC)
insidePeak := time.Date(2026, 8, 17, 6, 5, 0, 0, time.UTC)
if est, peak := EstimateAt(model, 400, 1000, false, justBeforePeak), EstimateAt(model, 400, 1000, false, insidePeak); est < peak {
t.Errorf("admitted at 05:30 UTC the reservation %v is under the peak-priced %v it could settle at", est, peak)
}

justBeforeFlip := RepriceAt.Add(-10 * time.Minute)
afterFlip := EstimateAt(model, 400, 1000, false, RepriceAt)
if est := EstimateAt(model, 400, 1000, false, justBeforeFlip); est < afterFlip {
t.Errorf("admitted before the flip the reservation %v is under the post-flip %v", est, afterFlip)
}

// And far from any boundary, off-peak reserves at the off-peak card,
// not at a permanent doubling.
quiet := time.Date(2026, 8, 17, 12, 0, 0, 0, time.UTC)
offPeak := costWith(cardFor(ratesOffPeak, model), Usage{InputTokens: 401, OutputTokens: 1000 + reasoningAllowance})
if est := EstimateAt(model, 400, 1000, false, quiet); math.Abs(est-offPeak) > 1e-12 {
t.Errorf("quiet off-peak reservation %v, want the off-peak card's %v", est, offPeak)
}
}

// If DeepSeek ships a model we have not priced, the unknown must be
// charged at the higher rate. Guessing low would let a new model become
// a way to spend the budget faster than it is counted.
Expand Down
Loading