dsh-session-cost-meter
Verifieddsh-session-cost-meter · v0.1.0 · MIT · Web UI
Per-session DeepSeek API cost meter with adaptive peak/off-peak (波峰/波谷) tariff
Install
dsh plugin add dsh-session-cost-meter Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Tags
Creators
Readme
dsh-session-cost-meter
A DeepSeek Harness profile bundle that answers "what did this session cost?" It shows a cost pill beside the composer, breaks the spend down by billing bucket, tariff window and model, and prices every request with the rate that was in force when that request was dispatched — so peak/off-peak (波峰/波谷) is handled per request instead of being guessed from a session total.
┌ 本次会话成本 ¥2.3691 ┐
│ 缓存命中 24,734,848 tok ¥0.9894 │
│ 未缓存输入 207,128 tok ¥0.4143 │
│ 输出 120,684 tok ¥0.9655 │
│ 合计 25,062,660 tok ¥2.3691 │
├───────────────────────────────────────────┤
│ 波峰 ¥2.3691 · 波谷 ¥0.0000 │
├───────────────────────────────────────────┤
│ deepseek-flash 25.1M tok ¥2.3691 │
├───────────────────────────────────────────┤
│ ● 当前时段:波峰 · 距切换 2h13m │
│ 高峰:周一至周五 09:00–12:00、14:00–18:00 │
│ 波谷价 = 波峰价 × 0.5 │
└───────────────────────────────────────────┘
Where it appears
One pill joins the existing token chip on the composer's ambient row
(conversation.composer.dock, occupant id session-cost, order 5, so it sits
immediately after the shipped stats entry). Clicking it opens the breakdown
above. All text is localised through the Client locale service (zh + en), and
all styling uses only --dsw-alias-* theme tokens, so it follows the host theme
and light/dark switching.
Pricing model
Rates are per 1,000,000 tokens, quoted exactly as published — the CNY column is authoritative, and the USD column is stored separately rather than derived, because the page rounds it independently.
| model | 缓存命中 (hit) peak | hit off-peak | 未缓存输入 (miss) peak | miss off-peak | 输出 (output) peak | output off-peak |
|---|---|---|---|---|---|---|
deepseek-flash (CNY) |
0.04 | 0.02 | 2.00 | 1.00 | 8.00 | 4.00 |
deepseek-flash (USD) |
0.006 | 0.003 | 0.30 | 0.15 | 1.20 | 0.60 |
deepseek-v4-pro (CNY) |
0.30 | 0.15 | 9.00 | 4.50 | 27.00 | 13.50 |
deepseek-v4-pro (USD) |
0.044 | 0.022 | 1.32 | 0.66 | 3.96 | 1.98 |
Source: https://api-docs.deepseek.com/zh-cn/quick_start/pricing/ (CNY) and https://api-docs.deepseek.com/quick_start/pricing/ (USD), effective 2026-09-10 04:00 UTC. Off-peak is exactly half of peak for every model and every bucket.
Peak (波峰) / off-peak (波谷)
The rule this implements, verbatim from DeepSeek:
闲时段价格为高峰时段价格的一半。北京时间周一至周五(不含中国法定节假日)9:00 - 12:00、14:00 - 18:00 为高峰时段;其余时段,包括周末及中国法定节假日全天均为空闲时段。
Peak is Monday–Friday, 09:00–12:00 and 14:00–18:00 Beijing time (01:00–04:00 and 06:00–10:00 UTC), excluding Chinese public holidays. Everything else — every evening, every night, all weekend, and all holiday time — is off-peak at half price.
DeepSeek publishes the windows but never the holiday calendar, so the plugin
ships it: PRC_HOLIDAYS_2026 in lib/tariff.js carries the 33 published 2026
dates from 《国务院办公厅关于2026年部分节假日安排的通知》(郭办发明电〔2025〕7 号), and
the bundle patch repeats the same list so an already-installed plugin is correct
without waiting for a restart (a test asserts the two lists are identical).
Make-up working days stay off-peak. The notice designates Saturday/Sunday
workdays in 2026 (01-04, 02-14, 02-28, 05-09, 09-20, 10-10). Those are
deliberately not holidays and not peak: the rule defines peak as
Monday–Friday and says every other period, "including weekends", is off-peak.
peakWeekdays already excludes them.
Holidays are off-peak for the whole local day, so on 2026-10-01 the
09:00–12:00 and 14:00–18:00 windows bill at half price. Without the calendar,
holiday hours would be billed at peak — roughly doubling the reported cost —
so the panel warns in two cases: no calendar at all, or a calendar that no longer
covers the year the tariff clock is in (holidayYears is published for this).
A user's config.holidays replaces the shipped list entirely.
How each request is priced
cost = cacheMiss × missRate + cacheHit × hitRate + cacheWrite × writeRate + output × outputRate- The four buckets are the harness's disjoint counts. DeepSeek's API reports a
cache-inclusive
prompt_tokens; the adapter already subtracts cache hits, soinputTokensis the cache-miss count and nothing is charged twice. reasoningTokensis a subset ofoutputTokensand is deliberately not charged again.cacheWriteis reported by the API but not billed, so its rate is 0.- The tariff instant for a request is its
step/starttime, not theassistant/messagetime. The settlement message is appended after the stream finishes, so a request dispatched at 11:59 Beijing and settled at 12:01 would otherwise be billed in the wrong window. Without astep/startthe settlement time is used. - A model with no published rate keeps its tokens but adds no money; the panel then warns how many tokens were excluded instead of inventing a price.
- Attempt samples are replaced, not accumulated: a retried request is billed once per attempt, and a repeat usage sample for the same attempt does not double count.
Configuration
Tunables live in the plugin row's config, which the profile's own patch layer
can override without touching this bundle:
# ~/.dsh/profiles/<profile>/cordis.patch.yml
- id: session-cost
config:
currency: USD # CNY (default) or USD
holidays: # replaces the shipped calendar entirely
- 2027-01-01
- 2027-01-02
rates:
offPeakMultiplier: 0.5 # off-peak multiplier
models:
deepseek-flash:
output: 8 # override one rate
my-model:
cacheHit: 0.01
cacheMiss: 0.5
cacheWrite: 0
output: 1
Config is a Standard Schema validator, so a misspelled key, a negative rate, a
malformed date or an out-of-range window fails the plugin loudly at startup
instead of silently pricing the session with defaults. Because it is not a
schemastery schema, the Plugin Manager page shows these fields as unknown
configuration (status: unsupported) rather than rendering a form; the values
still work.
⚠️ The shipped calendar covers 2026 only. The State Council publishes each
year's arrangement in the previous November, so around 2027-01-01 this list must
be extended (or replaced via config.holidays). Until then a 2027 statutory
holiday would be priced as a peak weekday, doubling the reported cost for
those days — and the panel says exactly that when the tariff clock leaves the
covered years.
Install
From the registry, by bundle name:
plugin_manager install_bundle target: dsh-session-cost-meter
or from a checkout / tarball, by absolute path:
plugin_manager install_bundle target: <absolute path to this directory>
Either form runs pnpm add <spec> in the active profile, appends the bundle to
dsh.profile.bundles, and applies the change live (application: applied). The
same is available from the CLI as dsh plugin --profile <name> add <spec>, and
from the Web UI's Plugins settings page, which lists the bundle once installed and
lets you disable it. Add - id: session-cost disabled: true to the profile patch
layer to turn it off without uninstalling.
Uninstall
plugin_manager remove_bundle target: dsh-session-cost-meter
Releasing
The package is a bundle: an npm package whose manifest declares
dsh.bundle and whose cordis.patch.yml inserts the loader row. Publishing is
therefore an ordinary npm publish, and private must stay absent. The prepack
script runs the whole suite, so a broken package cannot be uploaded.
# authenticate once, against the registry that accepts new publishes
npm login --registry=https://registry.npmjs.org/
# then, from the repo root
pnpm pack # optional: inspect the tarball first
pnpm publish --registry=https://registry.npmjs.org/
pnpm publish --dry-run performs the full flow without uploading and is the safe
way to check a change to the manifest. Pass --no-git-checks only when
publishing from a detached or dirty tree.
Use the Harness's bundled Node 24 (load_workspace_dependencies) rather than an
older shell Node: [email protected] on Node 23.0.0 mangles paths inside npm pack
(ib/client.js ENOENT) and cannot produce a tarball at all. The configured
~/.npmrc here points at registry.npmmirror.com, a read-only mirror — always
name --registry=https://registry.npmjs.org/ explicitly when publishing.
tests/package.test.mjs is the release gate. A package name appears in three
places — the manifest, the loader row in cordis.patch.yml, and the Client
bundle's window.__ModuleLoader__.load({ id }) — and the suite fails unless all
three agree, so a rename cannot ship half-applied.
Verification status
- 50 unit tests (
node --test "tests/*.test.mjs") cover the published windows against fixed instants, a minute-by-minute sweep of the schedule the Client reads against the host rules, the host/Client pure-function copies against each other, the per-bucket arithmetic, replacement/retry semantics, reference stability, schema validation, config validation, the shipped 2026 holiday calendar (count, format, no duplicates, make-up weekends excluded) and the panel's covered/stale/missing holiday reporting. - A real
deepseek-flashsession log (193 billing-relevant events, metadata only — no message content) folds to the totals computed independently from the same log: ¥1.16035704 over 10,816,537 tokens in 96 attempts. - Live in the running profile: the host fiber is
active, the Client occupantsession-costis registered onconversation.composer.dockbesidestats, and thesessionCostrow is checkpointed in the projection cache with values whose arithmetic checks out exactly (¥2.36912192 = 0.98939392 + 0.414256 + 0.965472). An independent fold of the durable log reproduced that row exactly (160 attempts, 28,216,576 / 209,990 / 131,665 tokens, ¥2.60196304). - The holiday calendar was confirmed live after re-applying the row: the running
projection reports all 33 published 2026 dates,
2026-01-01through2026-10-07, so the National Day closure now bills off-peak. - Not verified visually: no browser control is available in this environment, so the panel's rendered appearance has not been observed; only its syntax, its manifest, its live slot registration and its data are verified.
- Host-side code changes need an app restart to take effect (client bundles hot-reload). The first install is live immediately; a row toggle re-applies patch-level config without a restart.
Design notes
- The host half is a
sessionCostsession projection. It registers no services, subscribes to nothing, and imports no packages — not even Cordis or a schema library — so it cannot disagree with the host about a module instance.apply(state, event)returns the same state reference for events it ignores, andwire.viewreturns the same object when the view content is unchanged, so an unrelated event never re-renders a client. - The client half derives the current tier from a schedule the host sends (the tier in force plus its flip instants) rather than re-implementing the window rules; the only duplicated functions are a five-line parity lookup and a bucket sum, and a test extracts both from the bundle and checks them directly.
- No Harness Client package is imported (
reactandreact-domfrom the browser module table only), which is why the pill draws its own icon and copies the shipped panel's spacing instead of importing primitives. - Text color convention:
--dsw-alias-label-secondaryis the resting tone for labels, section headings, notes and the pill;--dsw-alias-label-primaryis reserved for values and names. The--dsw-alias-state-*tokens are indicator colors only (the tier dot, the unpriced warning) — using one for ordinary body text renders a near-invisible gray, which is a mistake this plugin made in its first revision. - A per-model row's token total is summed from its buckets when the host did not
publish a
total, so a running host that still holds its previous module generation renders correctly.
Known limitations
- Holiday calendar — DeepSeek never publishes it; the plugin ships the 2026
list from the State Council notice and must be extended for each new year once
the arrangement is published (see above).
config.holidaysreplaces it. - Make-up working days — treated as off-peak, following the rule's wording that peak is Monday–Friday and weekends are off-peak. If a future DeepSeek notice bills makeup workdays at peak, that cannot be expressed by a weekly weekday mask and would need a per-date override.
deepseek-v4-prorouting conflict — DeepSeek's news post (2026-09-10) says V4-Pro requests route to V4.1-Flash at Flash rates from 2026-09-14, while the changelog from the same date says V4-Pro keeps serving at unchanged billing. This plugin prices V4-Pro at its own published rates; overriderates.models.deepseek-v4-proif your account bills it as Flash.- Retired ids —
deepseek-chatanddeepseek-reasonerwere discontinued (2026-07-24) and have no current published rate, so they are reported as unpriced rather than guessed.deepseek-v4-flashanddeepseek-v4-flash-vision-expare aliased todeepseek-flash, which is what the provider bills them at. - Rate drift — DeepSeek can change prices; this table is a snapshot. Override
it in config rather than editing
lib/tariff.js. - Schedule horizon — the Client's tariff schedule covers 14 days, refreshed on every session event. Past that horizon with no new events at all, the pill falls back to the host's last computed tier.
- Prompt-caching effects — cost follows provider-reported cache hits, so a change in cache behaviour changes the bill, not the estimate. No request is predicted or pre-priced.
Prior art
@goodandready/dsh-cost-meter (MIT, third-party) is an independently published
DSH plugin with the same goal; this bundle was written from the running harness's
own APIs and does not depend on it.