You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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}
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):
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:
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.
Store peak. Safer for anyone budgeting, wrong the other way for 85% of hours.
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.
On 2026-08-16 DeepSeek moved V4 to peak / off-peak billing. The pricing table gained an extra
OFF-PEAK/PEAKcolumn between the row label and the first model's price, which shifts every price cell one position right.parsePricingPagereads them positionally, so each model now gets the price of the model to its left.I ran the current
parsePricingPageverbatim against the page as it is served today. Output:Correct values from https://api-docs.deepseek.com/quick_start/pricing/, USD per 1M, off-peak:
deepseek-v4-flashdeepseek-v4-prodeepseek-v4-flash-vision-expSo
deepseek-v4-progets flash's card (3× too cheap) anddeepseek-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):
cellPrice(row[i + 2])ati = 0now lands on the literal stringOFF-PEAK.extractPricefinds no$, returnsundefined, and the loopcontinues — so index 0 is skipped rather than misparsed, and every subsequent index reads its left neighbour's cell. ThePEAKrows are harmless:priceNameis"PEAK", which matches none of theCACHE HIT/CACHE MISS/INPUT/OUTPUTtests, so they are dropped.The comment above
cellPriceis now describing a page that no longer exists:That in-cell discount format is gone; the discount became a column.
Why none of the guards catch it
FALLBACK_MODELSonly engages onparsed.length === 0. Three models parse, so it never fires.deepseek-v4-flashkeeps its stale prices. Withinput/outputbothundefined,pricingisundefined, soif (m.pricing)skips the pricing block andpackages/data/providers/deepseek/models/deepseek-v4-flash.jsonkeeps0.14 / 0.28 / 0.0028— the pre-2026-08-16 card.deepseek-v4-pro.jsoncurrently readslast_updated: "2026-08-16"while holding0.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.tsstructurally 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.tsreports rather than gates. A 3× move in both directions would land inchanges.jsonlas 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,parseTokenCountandstripFootnoteunchanged 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.pricingis{ input; output; cached_input }andbuildPricingproduces one number per field. DeepSeek now publishes two numbers per field, selected by wall-clock time:That is 25 of every 168 hours at peak; weekends are off-peak end to end. Three ways I can see:
packages/npmand 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-proby 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/