GIN0076/dsh-token-usage โโ 0
@local/token-usage
๐ Token usage dashboard for DeepSeek Harness Settings โ daily/weekly/monthly per-model token stats with line & bar charts. Local-first, zero deps, pure SVG. DSH ่ฎพ็ฝฎ้ขๆฟ่ฏๅ ็จ้็ป่ฎกๆไปถ ้ๅ้่ฆ็ด่ง็ๆงๆฏๆฅใๆฏๅจใๆฏๆๆจกๅTokenๆถ่็็จๆทใ
Install
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:GIN0076/dsh-token-usageREADME
Read the full README โ๐ Token Usage for DSH
Bolt a fuel gauge onto your DeepSeek Harness Settings panel โ see exactly how many tokens every model burns, every day / week / month.
โจ Why you need this
DSH works hard for you โ but do you know what it costs you? The account page shows a balance, and raw logs are a pile of JSONL...
Now just open Settings โ ๐ Token Usage and the answer draws itself:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ๐ Token Usage ( Day | Week | Month ) [Last 30d โพ] โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โโโโโโโโโโโโโ โโโโโโโโโโโโโ โโโโโโโโโโโโโ โโโโโโโโโโโโโ โ
โ โ Total ๐งฎ โ โ Input โฌ๏ธ โ โ Output โฌ๏ธ โ โ Calls ๐ โ โ
โ โ 986.2M โ โ 610.0M โ โ 8.4M โ โ 947 โ โ
โ โโโโโโโโโโโโโ โโโโโโโโโโโโโ โโโโโโโโโโโโโ โโโโโโโโโโโโโ โ
โ โ
โ ๐ Trend (line chart) โฆ hover any day โ per-model details โ
โ 80M โค โญโโฎ โ
โ 40M โค โญโโโฎ โญโโโฏ โฐโโโฎ โโโ total โ
โ โผโโโโดโโโดโโโดโโโโโโโโดโโโ โโโ alpha-chat โ
โ 06-02 06-06 06-10 โโโ beta-reason โ
โ โ
โ ๐ Model ranking (bar chart) โ
โ alpha-chat โโโโโโโโโโโโโโโโโโโโ 62.4% โ
โ beta-reason โโโโโโโโโโโโโโโโโโโโ 31.8% โ
โ gamma-mini โ 5.8% โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
๐ผ๏ธ Schematic of the UI. The real thing follows DSH's theme tokens and looks great in both light and dark mode.
๐ฏ Features
| Area | What you get |
|---|---|
| ๐ Granularity | Flip between Day / Week / Month โ Monday-start weeks, calendar months; only day buckets are stored, so switching is instant and free |
| ๐๏ธ Custom range | Presets (last 7 / 30 days, 12 weeks, 12 months, all time) + pick your own start & end dates โ chart any window you like |
| ๐ Line chart | Bold total line + thin per-model lines, crosshair hover breaks down every day, click legend chips to toggle models |
| ๐ Bar chart | Horizontal model ranking with share % โ spot your biggest token sink at a glance ๐ธ |
| ๐ Rebuild | One-click full re-scan โ idempotent, watermarks guard against double-counting and gaps |
| ๐ก Accounting | Dirty counters fail closed, retried attempts are billed too, fork inheritance never double-counts, compaction is included |
๐ Install
Option one ยท straight from GitHub (recommended, DSH's standard channel)
dsh plugin --profile web add github:GIN0076/dsh-token-usage
Option two ยท clone & install locally (works offline; the channel this repo was built on)
git clone https://github.com/GIN0076/dsh-token-usage.git
Then run plugin_manager install_bundle in DSH with the clone directory as target, or use
Plugins page โ Install Bundle in the GUI.
(Got ambiguous-install? remove_bundle first, then install โ a known leftover-link quirk.)
Hard-refresh the page (Ctrl+Shift+R) โ open Settings โ ๐ Token Usage ๐
Uninstall: plugin_manager remove_bundle โ @local/token-usage โ zero host residue;
the data folder is yours to keep or delete.
๐ค How it works
~/.dsh/sessions session logs (the single source of truth, read-only)
โ โ live fold of session/event โก startup backfill (watermark-idempotent,
โผ skips sessions whose bytes never changed)
Host half โโโถ day ร provider/model ร six buckets โโโถ storage-domain (persistent)
โ (corrupt? backup-and-skip + rebuild)
โผ
/token-usage-rpc ๐ connection auth + loopback Host + same-origin Origin
โผ same-origin fetch (data never leaves your machine)
Client half โโโถ Settings section + pure-SVG charts (no chart lib, no build, zero deps)
Accounting details (stats.js pure functions, guarded by 46 fixtures):
- โ
Counted:
assistant/message(including stream usage),assistant/attemptโ retries cost money too!,compaction/summary - ๐ท๏ธ Attribution: messages carry their own provider/model; attempts & compaction fall back to the latest request header
- ๐ซ Skipped: unsafe integers, negatives, reasoning > output, totals that contradict buckets
- ๐ Buckets use the host's local timezone; fork-inherited prefixes are cut by
inheritedEventCountโ your ancestors' tokens are never counted twice
๐ Privacy
- Local-first: statistics come only from
~/.dsh/sessionson this machine โ no network calls, no uploads, ever - Triple RPC fence: connection auth (cookie) + loopback Host + same-origin Origin
- MIT licensed. No telemetry, no accounts, no backdoors.
๐งฉ Architecture (for the tinkerer)
~/.dsh/sessions session logs (the single source of truth, read-only)
โ โ live: ctx.on('session/event') folds post-commit events
โ โก backfill: sessionQuery.listSessions + readSession (watermark-idempotent;
โ skip sessions whose file bytes are unchanged)
โผ
Host half host.js
ยท storage-domain `token_usage` (per-record + backup-and-skip; path-safe base64url keys)
- daily: day ร provider/model ร six counters
- watermark: per-session { seq, route, bytes }
ยท /token-usage-rpc exact route (connection auth + loopback Host + same-origin Origin)
- stats {granularity, fromDay, toDay} โ aggregation (stats.js pure functions)
- status โ { backfill, rebuilding, storageOk, dataSpan }
- rebuild โ full re-scan (pauses live folding + buffered replay to avoid races)
โผ same-origin POST fetch
Client half client.js (static bundle, __ModuleLoader__)
ยท settings.section entry (id: token-usage, order: 50)
ยท Day/Week/Month segmented control + presets (7d/30d/12w/12m/all/custom dates) + rebuild
ยท summary cards โ trend line chart (bold total + per-model lines + crosshair + legend)
โ model ranking bar chart
| File | Responsibility |
|---|---|
stats.js | Pure aggregation; node stats.fixtures.mjs runs 46 fixtures (accounting / dedup / week-month buckets / custom ranges / rollup consistency) |
host.js | Host half: folding, backfill, rebuild, storage, RPC |
client.js | Client half: section, controls, two SVG charts |
cordis.patch.yml | Bundle patch row (relative specifier ./host.js) |
locale/{zh,en}.json | Plugin Manager display metadata; section copy lives inline in client.js |
๐ ๏ธ Developer cheat sheet
| Want toโฆ | Do this |
|---|---|
| Change the Client half (UI / charts) | Edit client.js โ hard-refresh the page (client-hmr swaps the rev) |
| Change the Host half (stats / RPC) | Edit host.js โ restart DSH; hot-editing a running host is limited by Node's per-URL ESM cache, so swap the filename to force a new generation (see below) |
| Run tests | node stats.fixtures.mjs (46 fixtures) |
| Syntax check | node --check host.js && node --check client.js && node --check stats.js |
Host hot-reload recipe (running, no restart): edit host.js โ Copy-Item host.js host2.js
โ point the cordis.patch.yml row name at './host2.js' โ remove_bundle + install_bundle.
Why: Node caches ESM per URL in-process; a new filename = a fresh URL = freshly loaded code.
Fresh installs and restarts are not affected by this at all.
โ ๏ธ Pitfalls we hit (all fixed; kept here as a field manual)
| Pitfall | Symptom | Root cause & fix |
|---|---|---|
connection not injected | Every RPC returns an empty 400 | Cordis Context is a strict proxy: touching a non-injected service throws, and the webserver's catch-all turns it into 400. Fix: add 'connection' to inject (same as open-in-app) |
| Broken disposer | Domain stuck already-open after every remove | ctx.inject() returns a fiber, not a function โ calling it threw a TypeError and aborted dom.close(), leaking the reservation. Fix: try/catch per step, close first, let the parent ctx cascade child fibers |
| Inconsistent error check | Retry gave up after one attempt | DomainError.code='already-open' (hyphen) vs message="โฆ is already open" (space) โ check both |
| One-shot storage failure | storageOk:false forever | Added lazy recovery: retry attachStorage on later requests; when memory already holds data, skip hydrate (double-count guard) and overwrite disk instead |
| Ghost domain | A dead generation holds the reservation | storageDomain.get(name) returns the leaked handle โ close it directly (the facility is a singleton, so the holder must be a dead fiber); now built into the retry path |
๐ฆ Restoring after a DSH update
Yes โ one command. The plugin source lives in your workspace, not in ~/.dsh, so an
update never touches it; only the profile registration is cleared. Re-run the install command
(remove first if you hit ambiguous-install). No peer constraints: the bundle declares no
@deepseek-ai/dsh-* peers, so compatibility gates never block it, and every API it uses
(settings.section / sessionQuery / storageDomain / webServer / connection /
session/event) is stable upstream surface. After an update, run the four-step check:
list_pluginsโinclude:token-usageshould befiberPhase: active- RPC returns 401 unauthenticated / 200 authenticated
- Hard-refresh โ both charts render in Settings
- If you edited the Host half, confirm the filename in
cordis.patch.ymlstill exists
Data: statistics are derived. Even if ~/.dsh is wiped, restoring the session logs makes
the startup backfill rebuild everything โ or hit "Rebuild" for a full re-scan. The source
of truth can't be lost, so the aggregates can always grow back.
๐ License
MIT ยฉ 2026 GIN0076 โ issues and PRs welcome.
Inspired by the usage panel in ZCode Usage Stats and the accounting design of local-first trackers like ccusage / tokscale / token-history.