Skip to content

fetch-deepseek: DeepSeek's new peak/off-peak column shifts every price one model to the right #101

Description

@xyzs996

On 2026-08-16 DeepSeek moved V4 to peak / off-peak billing. The pricing table gained an extra OFF-PEAK / PEAK column between the row label and the first model's price, which shifts every price cell one position right. parsePricingPage reads them positionally, so each model now gets the price of the model to its left.

I ran the current parsePricingPage verbatim against the page as it is served today. Output:

parsed.length = 3            ← FALLBACK_MODELS does not engage

deepseek-v4-flash              undefined
deepseek-v4-pro                {"input":0.22,"output":0.66,"cached_input":0.007}
deepseek-v4-flash-vision-exp   {"input":0.66,"output":1.98,"cached_input":0.022}

Correct values from https://api-docs.deepseek.com/quick_start/pricing/, USD per 1M, off-peak:

input (miss) output cached input
deepseek-v4-flash 0.22 0.66 0.007
deepseek-v4-pro 0.66 1.98 0.022
deepseek-v4-flash-vision-exp 0.22 0.66 0.007

So deepseek-v4-pro gets flash's card (3× too cheap) and deepseek-v4-flash-vision-exp — which is priced identically to flash — gets pro's card (3× too expensive). Both are wrong in a way that looks entirely plausible on the page.

Why it shifts

The table rows are now shaped like this (tags stripped):

PRICING(1)(2) | 1M INPUT TOKENS(CACHE HIT) | OFF-PEAK | $0.007 | $0.022 | $0.007
PEAK          | $0.014 | $0.044 | $0.014
1M INPUT TOKENS(CACHE MISS) | OFF-PEAK | $0.22 | $0.66 | $0.22
PEAK          | $0.44 | $1.32 | $0.44
1M OUTPUT TOKENS | OFF-PEAK | $0.66 | $1.98 | $0.66
PEAK          | $1.32 | $3.96 | $1.32

cellPrice(row[i + 2]) at i = 0 now lands on the literal string OFF-PEAK. extractPrice finds no $, returns undefined, and the loop continues — so index 0 is skipped rather than misparsed, and every subsequent index reads its left neighbour's cell. The PEAK rows are harmless: priceName is "PEAK", which matches none of the CACHE HIT / CACHE MISS / INPUT / OUTPUT tests, so they are dropped.

The comment above cellPrice is now describing a page that no longer exists:

// Pricing cells can look like "$0.003625 (75% off(3))$0.0145" — the first $
// is the current (discounted) price, which is what we want.

That in-cell discount format is gone; the discount became a column.

Why none of the guards catch it

  • FALLBACK_MODELS only engages on parsed.length === 0. Three models parse, so it never fires.
  • deepseek-v4-flash keeps its stale prices. With input/output both undefined, pricing is undefined, so if (m.pricing) skips the pricing block and packages/data/providers/deepseek/models/deepseek-v4-flash.json keeps 0.14 / 0.28 / 0.0028 — the pre-2026-08-16 card. deepseek-v4-pro.json currently reads last_updated: "2026-08-16" while holding 0.435 / 0.87 / 0.003625, i.e. it is stamped with the date of the change and holds the values from before it.
  • check-freshness.ts structurally cannot see this. Its own note says so: "Price cannot corroborate: the same model is priced differently per provider, which is the point of tracking price at all." It compares model presence against aggregator witnesses — and this run would make DeepSeek look healthier, because it adds a genuinely new model (deepseek-v4-flash-vision-exp) that the witnesses also list.
  • detect-changes.ts reports rather than gates. A 3× move in both directions would land in changes.jsonl as an ordinary price change, and the price-history feature from feat: track and surface model price history #87 would then surface it to users as a real DeepSeek reprice.

Every health signal reads green.

Repro

curl -s https://api-docs.deepseek.com/quick_start/pricing/ -o /tmp/ds.html
# then call parsePricingPage(fs.readFileSync("/tmp/ds.html","utf-8"))

I ran it by copying parsePricingPage, findHtmlTables, parseHtmlTable, stripHtml, extractPrice, parseTokenCount and stripFootnote unchanged into one file — no edits — and got the output above.

Fixing it needs a schema decision, which is why this is an issue and not a PR

The parser side is small: when the cell at the model-0 position parses to no price and reads OFF-PEAK / PEAK, the row has a tier column and the offset is one greater. Something like keying off the tier label rather than assuming a fixed offset would also survive the column being dropped again later.

The harder half is that ParsedModel.pricing is { input; output; cached_input } and buildPricing produces one number per field. DeepSeek now publishes two numbers per field, selected by wall-clock time:

Off-peak rates are half of the peak rates. Peak hours are 01:00 - 04:00 and 06:00 - 10:00 UTC, Monday through Friday (all other hours are off-peak).

That is 25 of every 168 hours at peak; weekends are off-peak end to end. Three ways I can see:

  1. Store off-peak and note it. Off-peak covers 143 of 168 hours, so it is the better single number, and it keeps the schema untouched. Every consumer is then quietly 2× low for 15% of the week.
  2. Store peak. Safer for anyone budgeting, wrong the other way for 85% of hours.
  3. Add an optional time-of-use field to the pricing schema. Correct, and it would generalise — DeepSeek is not likely to be the last provider to do this — but it is a schema change that touches packages/npm and the web app, not just the fetcher.

If (1) or (2), the fix is contained enough that I am happy to send a PR with the parser change and the three corrected model files. I did not open one because picking among these is your call, and the parser fix alone would write a confidently wrong number under any of them.

One thing worth doing regardless of which route: a sanity check that refuses to write when a model's price moves by more than some factor without the model id changing. This bug moved deepseek-v4-pro by 3× in one run, and nothing in the pipeline had an opinion about that.

The vision model is also new to the catalog (deepseek-v4-flash-vision-exp, image input, priced identically to flash) — worth adding, just not with pro's numbers.

Per-vendor cache-read rates I checked these against: https://xyzs996.github.io/llm-api-pricing/

Track in Rupic

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions