# ๐Ÿ“Š Token Usage for DSH **Bolt a fuel gauge onto your [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) Settings panel โ€”** **see exactly how many tokens every model burns, every day / week / month.** [![License: MIT](https://img.shields.io/badge/License-MIT-ffd93d?style=flat-square&labelColor=2b2b2b)](LICENSE) [![DSH Plugin](https://img.shields.io/badge/DSH-Plugin-4f8ef7?style=flat-square&labelColor=2b2b2b)](#-install) [![Version](https://img.shields.io/badge/version-1.3.0-22b07d?style=flat-square&labelColor=2b2b2b)](CHANGELOG.md) [![Zero Deps](https://img.shields.io/badge/zero--deps-๐ŸŸฉ_pure_SVG-e0566b?style=flat-square&labelColor=2b2b2b)](#-how-it-works) [![Local Only](https://img.shields.io/badge/local--first-๐Ÿ”’_nothing_leaves_your_machine-9b6bff?style=flat-square&labelColor=2b2b2b)](#-privacy) [English](README.md) ยท [็ฎ€ไฝ“ไธญๆ–‡](README.zh.md)
--- ## โœจ 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: ```text โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ ๐Ÿ“Š 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: ```json { "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** (`currency` defaults to ยฅ); model keys are the `provider/model` strings shown in the panel - `cacheRead` / `cacheWrite` fall back to the `input` price; `default` covers 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)** ```powershell # 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 web` from older docs: that is the browser profile, while the desktop app > reads `desktop` โ€” 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)** ```powershell 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 ```text ~/.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/attempt` goes 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/sessions` on 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) ```text ~/.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: 1. `list_plugins` โ†’ `include:token-usage` should be `fiberPhase: active` 2. RPC returns **401** unauthenticated / **200** authenticated 3. Hard-refresh โ†’ both charts render in Settings 4. If you edited the Host half, confirm the filename in `cordis.patch.yml` still 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](LICENSE) ยฉ 2026 GIN0076 โ€” issues and PRs welcome. *Inspired by the usage panel in [ZCode Usage Stats](https://zcode.z.ai/en/docs/usage-stats) and the accounting design of local-first trackers like ccusage / tokscale / token-history.*