๐ 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 โพ] [Export] [Prices] [Settings] [โป Rebuild] โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โโโโโโโโโโ โโโโโโโโโโ โโโโโโโโโโ โโโโโโโโโโ โโโโโโโโโโ โโโโโโโโโโ โ
โ โ Total๐งฎโ โ ยฅ Cost โ โInput โฌ๏ธโ โOutputโฌ๏ธโ โCache rdโ โCache % โ โ
โ โ 1.14B โ โ ยฅ3.42 โ โ 9.46M โ โ 1.46M โ โ 1.13B โ โ 99.2% โ โ
โ โโโโโโโโโโ โโโโโโโโโโ โโโโโโโโโโ โโโโโโโโโโ โโโโโโโโโโ โโโโโโโโโโ โ
โ โโโโโโโโโโ โโโโโโโโโโ โโโโโโโโโโ โโโโโโโโโโ โโโโโโโโโโ โ
โ โ Today โ โ Month โ โ Budget โ โ Calls โ โBuckets โ โ turns red when โ
โ โ 22.6M โ โ 1.14B โ โยฅ1.5/ยฅ10โ โ 3239 โ โ 3 โ over budget โ
โ โโโโโโโโโโ โโโโโโโโโโ โโโโโโโโโโ โโโโโโโโโโ โโโโโโโโโโ โ
โ โ
โ ๐ Token Usage ยท Day ยท 3 buckets 2026-09-29 โ 2026-10-01 (3d) ยท hover โ
โ 80M โค โญโโฎ โ
โ 40M โค โญโโโฎ โญโโโฏ โฐโโโฎ โโโ total โ
โ โผโโโโดโโโดโโโดโโโโโโโโดโโโ โโโ alpha-chat โ
โ 06-02 06-06 06-10 โโโ beta-reason โ
โ โ
โ ๐ Ranking ยท share ( By model | By provider ) [By cost โพ] โ
โ alpha-chat โโโโโโโโโโโโโโโโโโโโ 62.4% ยท ยฅ2.13 โ
โ beta-reason โโโโโโโโโโโโโโโโโโโโ 31.8% ยท ยฅ1.09 โ
โ gamma-mini โ 5.8% ยท ยฅ0.20 โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
๐ผ๏ธ Schematic of the UI. Cost / Budget / Today / Month cards only appear once you configure a price table (and optionally a budget). 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. Note: the summary cards and the ranking are totals for the whole range (they do not change with granularity) โ what changes is the line chart grouping and the Buckets card |
| ๐๏ธ 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 % and cost โ spot your biggest token sink at a glance ๐ธ |
| ๐ฐ Cost estimate | Bring your own price table (~/.dsh/token-usage-prices.json, editable in the panel) priced per million tokens; no prices are baked in, so cost stays hidden until you configure it |
| ๐ธ Budget & today/month | Cards for today / this month (fixed windows); with a budget and a price table you also get a budget card (spent / limit) that turns red when over |
| ๐ท๏ธ Multiple dimensions | Ranking and line chart toggle by model / by provider; ranking can also sort by cost once priced |
| โ๏ธ Configurable | Edit ~/.dsh/token-usage-config.json right from the panel's Settings: refresh interval / budget / retention |
| ๐ Numbers | Cards: total / cost / input / output / cache read / cache write / reasoning / cache hit rate / calls / models |
| ๐ค Export | Current range as CSV (bucket ร model rows + model subtotals + grand total) or JSON, with copy & download |
| ๐ Auto refresh | Every 60s while the tab is visible (silent, no spinner), instant refresh when you come back, zero requests while hidden |
| ๐ Rebuild | One-click full re-scan โ idempotent, watermarks guard against double-counting and gaps; if the store holds history whose session logs are gone, a confirmation is required first (that part cannot be rebuilt) |
| ๐ก Accounting | Dirty counters fail closed, compaction summaries are attributed by their own provider/model, fork inheritance never double-counts |
๐ฐ How cost is computed (optional)
No prices are baked in โ someone else's price list is not your bill. Fill in a table from the panel's Prices button:
{
"currency": "$",
"models": {
"deepseek-account/deepseek-flash": { "input": 1, "output": 2, "cacheRead": 0.1, "cacheWrite": 1 }
},
"default": { "input": 1, "output": 2 }
}
- Unit = price per million tokens (
currencydefaults to ยฅ); model keys are theprovider/modelstrings shown in the panel cacheRead/cacheWritefall back to theinputprice;defaultcovers models you did not list- Stored at
~/.dsh/token-usage-prices.json(written as temp file + atomic rename, never half-written) - โInsert templateโ lists every model that appears in your current data so you only fill numbers
- Unpriced models are counted separately and the panel says so โ no silent zeros
- Clear the box and save to hide cost again
Cost is an estimate, not an invoice โ your provider's billing is the source of truth.
๐ Install
Option one ยท straight from GitHub (recommended, DSH's standard channel)
# desktop app (the profile the packaged app actually loads)
dsh plugin --profile desktop add github:GIN0076/dsh-token-usage
# or omit --profile to target the current default profile
dsh plugin add github:GIN0076/dsh-token-usage
โ ๏ธ Don't copy
--profile webfrom older docs: that is the browser profile, while the desktop app readsdesktopโ installing into the wrong profile looks like "command succeeded, panel missing".
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 50 fixtures):
- โ
Counted:
assistant/message(including stream usage),compaction/summary(attributed by the event's own provider/model) - โ ๏ธ Supported but not promised:
assistant/attemptgoes through the same folding path, but on 0.2.0-rc.2 that event carries no usage (18 sessions / 17 attempt events / 0 hits) โ the host never reports tokens for retried or failed calls, so retry cost is simply not observable here - ๐ท๏ธ Attribution: messages carry their own provider/model; summaries prefer their own, then fall back to the latest request header; attempts use 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, + cost when priced)
- status โ { backfill, rebuilding, storageOk, dataSpan, prices }
- rebuild โ full re-scan (pauses live folding + buffered replay to avoid races; **returns needsConfirm first when the store holds log-less history**)
- prices / prices.save โ read/write ~/.dsh/token-usage-prices.json (validated, atomic replace)
ยท auth/origin rejections carry a JSON diagnostic body {error, hint, seen{host,origin,site,cookie}} (never the cookie value)
โผ 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
ยท price-table editor (template / validation / save) ยท export sheet (CSV/JSON) ยท 60s auto refresh
| File | Responsibility |
|---|---|
stats.js |
Pure aggregation; node stats.fixtures.mjs runs 63 fixtures (accounting / dedup / routing / week-month buckets / custom ranges / rollup consistency / price table & cost) |
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 (63) + node client.i18n.fixtures.mjs (257) + node client.smoke.mjs (51) |
| 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.
No comments yet. Be the first to write one.