Skip to content

dsh-session-cost-meter

Verified

dsh-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, so inputTokens is the cache-miss count and nothing is charged twice.
  • reasoningTokens is a subset of outputTokens and is deliberately not charged again.
  • cacheWrite is reported by the API but not billed, so its rate is 0.
  • The tariff instant for a request is its step/start time, not the assistant/message time. 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 a step/start the 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-flash session 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 occupant session-cost is registered on conversation.composer.dock beside stats, and the sessionCost row 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-01 through 2026-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 sessionCost session 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, and wire.view returns 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 (react and react-dom from 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-secondary is the resting tone for labels, section headings, notes and the pill; --dsw-alias-label-primary is 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.holidays replaces 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-pro routing 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; override rates.models.deepseek-v4-pro if your account bills it as Flash.
  • Retired ids — deepseek-chat and deepseek-reasoner were discontinued (2026-07-24) and have no current published rate, so they are reported as unpriced rather than guessed. deepseek-v4-flash and deepseek-v4-flash-vision-exp are aliased to deepseek-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.