Bundle
dsh-token-bill
Live DeepSeek balance plus per-hour token costing for DSH: two agent tools, hourly bar charts, and Markdown/HTML/JSON statements.
- Source
- SanChou0627
- License
- MIT
- Updated
- Updated yesterday
Readme
# dsh-token-bill
English | [中文](README.zh.md)
Real-time token billing for **DeepSeek Harness (DSH)**: the live account balance, today's actual spend, per-hour token counts folded into cost bars, and a Markdown / HTML / JSON statement you can keep.
Two model-facing tools, no UI required, no extra daemon:
- **`token_status`** — live balance, today's observed spend, today's token buckets.
- **`token_bill`** — hourly bar charts, peak/valley split, per-hour detail table, and a written statement.
```
📊 2026-09-28 token bill
Balance ¥52.9100 · Today ¥4.6900 (ledger-observed)
Tokens 79.65M (input 1.02M · cache hit 78.14M · output 492.7K) · 98.1% cache hit · 716 calls
Busiest hour 15:00 (29.00M tokens) · Costliest hour 15:00 (¥2.3380)
Tokens per hour (6 hours with usage)
00:00 │█████████▃ │ 9.89M
01:00 │███████▂ │ 7.72M
11:00 │██▆ │ 2.92M
12:00 │██████████████▇ │ 15.80M
13:00 │█████████████▄ │ 14.33M
15:00 │██████████████████████████│ 29.00M
Cost per hour (calibrated to the observed daily charge)
00:00 │██████▃ │ 0.5607
01:00 │██▅ │ 0.2259
...
```
## Why this plugin exists
DSH deliberately treats token counts as **measurement, not as a billing record**: `ctx.tokenMeter` reports request pressure, nothing in the harness converts tokens into money, and it ships no price table at all. Meanwhile the three things you actually want are scattered:
| What you want | Where it lives | Authority |
|---|---|---|
| Account balance | `GET https://api.deepseek.com/user/balance` (API-key auth) | live, authoritative |
| What today actually cost | `$DSH_HOME/.dshw-usage.json` balance ledger | observed, authoritative |
| Tokens per hour | `$DSH_HOME/sessions/**/session.v*.jsonl.zstd` | provider-reported, exact |
`dsh-token-bill` joins them under one rule: **money is observed, tokens are measured.** The day's charge comes from the account's own balance movement, and the price table is used only to spread that observed total across the day's hours.
## Features
- 💰 **Live balance** from the official balance endpoint, falling back to the ledger's last observation (explicitly labelled) when the network fails.
- 📊 **Per-hour token counts** read from durable session logs — the four disjoint provider buckets (`inputTokens`, `cacheReadTokens`, `cacheWriteTokens`, `outputTokens`), not a character heuristic.
- 🧾 **Statements on disk**: self-contained HTML with inline SVG bar charts, Markdown for archiving, raw JSON for scripting.
- ⛰️ **Peak/valley aware**: Beijing-time peak windows, weekend and statutory-holiday valley rates, and a peak-vs-valley split in the report.
- 🎯 **Exact arithmetic**: money is carried as integer 1e-8 units, so `sum(hourly amounts) === daily amount` is an identity rather than a rounding accident.
- 🔒 **Read-only**: the plugin never writes to your ledger, session logs, or credentials.
## Install
```bash
# from a registry, once published
dsh plugin --profile web add dsh-token-bill
# from a local checkout
dsh plugin --profile web add link:/absolute/path/to/dsh-token-bill
```
Both tools then appear to the model in new sessions. If your profile does not reload on its own, restart the DSH web server afterwards.
**Requirements:** Node ≥ 22.12 (the plugin resolves its DSH peers through `require(esm)`), and a DSH profile with the `tools` service mounted (any standard Web profile).
## The balance ledger
Today's **actual** charge comes from a ledger at `$DSH_HOME/.dshw-usage.json`, written by the excellent [`dsh-whale-widget`](https://github.com/MeteorNOX/DeepSeek-Balance-Whale-Widget) balance widget — not by this plugin. Each day row records an opening balance, the last observed balance, and accumulated debits and credits:
```json
{
"openingUnits": 760000000,
"lastUnits": 5525000000,
"debitUnits": 388000000,
"creditUnits": 5000000000,
"firstAt": 1790529962822,
"lastAt": 1790580387568
}
```
Amounts are integer 1e-8 CNY units. The observed charge is the sum of balance *decreases*, so a top-up adds to `creditUnits` and can never inflate your spend. If the row also **closes** (`opening − debits + credits = last`), a top-up is reported as information; if it does not close, something moved the balance that the ledger could not classify, and the plugin says so rather than quietly reporting a wrong number.
**If you do not run that widget**, the ledger is simply absent and the plugin degrades to price-table estimates, clearly labelled as estimates — the balance still comes from the live endpoint.
## Tools
### `token_status`
| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `includeLedger` | boolean | `true` | Append the ledger's path, age, and event count. |
| `includePricing` | boolean | `false` | Append the peak/valley price table. |
Returns `day`, `balance`, `currency`, `balanceSource` (`api` or `ledger`), `balanceStale`, `todayObserved`, `todayTokenEstimate`, `totalTokens`, `inputTokens`, `cacheReadTokens`, `outputTokens`, `calls`, `ledgerPath`, `text`.
### `token_bill`
| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `day` | string | today | Statement date, `YYYY-MM-DD`, Beijing time. |
| `days` | integer | `1` | Also include a per-day summary for the last N days. |
| `costMode` | `ledger` \| `token` \| `both` | `ledger` | See [Cost model](#cost-model). |
| `write` | boolean | `true` | Write the statement files to disk. |
| `formats` | array of `html` \| `md` \| `json` | `["html","md"]` | Which statement files to write. |
Returns `day`, `amount`, `amountSource` (`ledger` or `token-estimate`), `currency`, `balance`, `totalTokens`, `calls`, `activeHourCount`, `peakTokens`, `valleyTokens`, `files`, and the full `text` rendering.
Files are written to `$DSH_HOME/token-bill/` as `token-bill-<day>.{html,md,json}`. The host process's working directory is the DSH installation itself, so the default is deliberately **not** `process.cwd()`; override it with `DSH_TOKEN_BILL_DIR` or the `outDir` config.
## Data sources
```
api.deepseek.com/user/balance ──► live balance ─┐
├──► report ──► bar charts + statement
$DSH_HOME/.dshw-usage.json ─────► observed ¥ ──┤
$DSH_HOME/sessions/**/*.zstd ───► exact tokens ─┘
```
**Balance** — `GET https://api.deepseek.com/user/balance` with `Authorization: Bearer <DEEPSEEK_API_KEY>`, returning `balance_infos[]` with `total_balance` / `granted_balance` / `topped_up_balance` per currency. This needs no browser session. The key is read from the `DEEPSEEK_API_KEY` environment variable (also `DSH_DEEPSEEK_API_KEY`, `DEEPSEEK_KEY`) or from `refs.DEEPSEEK_API_KEY` in `$DSH_HOME/.credentials.yaml`.
**Token usage** — every durable `assistant/message` event carries the adapter-reported `usage` alongside an epoch-millisecond `time`, which is what makes hourly accounting possible. Buckets are disjoint exactly as the provider reports them:
- `inputTokens` — uncached prompt input (a cache miss)
- `cacheReadTokens` — prompt input served from cache (a cache hit)
- `cacheWriteTokens` — prompt tokens written into the cache
- `outputTokens` — completion tokens, reasoning already included
Session logs are `session.v<generation>.jsonl.zstd` under `$DSH_HOME/sessions/<projectKey>/<sessionId>/`, header line first, with each append written as its own checksummed Zstandard frame. Older uncompressed `session.v<generation>.jsonl` logs are read too.
## Cost model
`costMode` decides where the money number comes from:
| Mode | Daily amount | Per-hour amounts | `token estimate` column |
|---|---|---|---|
| `ledger` (default) | observed charge from the ledger | apportioned, calibrated to that total | shown |
| `token` | price-table estimate | price-table estimate | n/a |
| `both` | observed charge from the ledger | apportioned, calibrated to that total | shown |
In `ledger` mode the per-hour amounts are computed by **largest-remainder apportionment at the statement's own precision**, so the hours a statement prints always add up to the total it prints. When the ledger has no observation for a day, the plugin falls back to a price-table estimate and labels it as such — it never presents an estimate as an observed charge.
### Peak / valley pricing
Beijing time. Peak (standard) rates apply **Mon–Fri 09:00–12:00 and 14:00–18:00**. Every other hour, plus all weekend days and all statutory holidays, bills at the valley rate (half the peak rate). Prices are CNY per million tokens, shown as `valley / peak`:
| Model | Cache hit | Cache miss | Output |
|---|---|---|---|
| `deepseek-flash`, `deepseek-v4-flash`, `deepseek-v4-flash-vision-exp`, `deepseek-chat` | 0.02 / 0.04 | 1 / 2 | 4 / 8 |
| `deepseek-pro`, `deepseek-v4-pro`, `deepseek-reasoner` | 0.15 / 0.3 | 4.5 / 9 | 13.5 / 27 |
An unrecognised model name falls back to the Flash series so a new model still produces a usable estimate. The statutory holiday list (`HOLIDAYS_2026`, 34 days) lives in `lib/pricing.mjs` and can be extended through config when a new State Council calendar is published.
## Configuration
Add a `config` block to the plugin's row in your profile's `cordis.patch.yml`:
```yaml
- id: token-bill
name: dsh-token-bill
config:
costMode: ledger # ledger | token | both
decimals: 4 # decimals used in printed amounts
chartWidth: 26 # bar width in terminal charts
historyDays: 1 # days included in the per-day summary
writeFiles: true # set false to never write statements
timeoutMs: 10000 # live balance request timeout
outDir: !!js dshHomePath('token-bill')
sessionsRoot: !!js dshHomePath('sessions')
# Override or extend the price table (CNY per million tokens):
# models:
# my-model: { hit: { off: 0.01, peak: 0.02 }, miss: { off: 1, peak: 2 }, out: { off: 4, peak: 8 } }
# Extra statutory holidays, billed at the valley rate all day:
# holidays: ['2027-01-01']
```
| Field | Default | Meaning |
|---|---|---|
| `dshHome` | `$DSH_HOME` or `~/.dsh` | DSH home directory. |
| `profile` | `$DSH_PROFILE` or `web` | Profile whose in-profile ledger path is also checked. |
| `sessionsRoot` | `<dshHome>/sessions` | Root of the session logs. |
| `outDir` | `$DSH_TOKEN_BILL_DIR` or `<dshHome>/token-bill` | Statement output directory. |
| `decimals` | `4` | Decimal places in printed amounts. |
| `chartWidth` | `26` | Bar width for terminal charts. |
| `costMode` | `ledger` | Money source, as above. |
| `historyDays` | `1` | Days in the per-day summary. |
| `maxBytes` | `134217728` (128 MB) | Skip any single session log larger than this. |
| `timeoutMs` | `10000` | Balance request timeout. |
| `writeFiles` | `true` | Whether statements may be written. |
| `models` | built-in table | Price table override. |
| `holidays` | `HOLIDAYS_2026` | Statutory holiday day keys billed at the valley rate. |
## Architecture
```
lib/index.js Plugin entry: name / inject / Config / apply, registers both tools
lib/service.mjs Orchestration: resolve config -> gather -> fold -> report -> write
lib/account.mjs API-key discovery, balance endpoint, live-to-ledger fallback policy
lib/ledger.mjs Read-only reader for $DSH_HOME/.dshw-usage.json
lib/logs.mjs Session enumeration, multi-frame zstd decode, hourly/daily folds
lib/pricing.mjs Peak/valley schedule and the DeepSeek price table
lib/money.mjs Exact decimal money (integer 1e-8 units)
lib/report.mjs Report assembly, terminal charts, HTML statement
lib/markdown.mjs Markdown statement
lib/chart.mjs Terminal bar charts
```
Implementation notes worth knowing:
- **Money never touches a binary float.** Every amount is an integer number of 1e-8 units. Model unit prices are tiny and accumulate over thousands of calls — exactly where floats drift — which is why the reconciliation identity holds.
- **Session logs are multi-frame Zstandard.** Node's `zstdDecompressSync` decodes only the first frame and reports no consumed length, so a naive read silently truncates a log to its header line. `decodeZstdFrames()` locates frames by their magic sequence and validates each slice by decoding it; a torn final append simply ends the scan, which is the correct behaviour for a log being written while it is read.
- **The ledger is read, not polled.** A balance widget is already observing your account on a timer; reusing its record avoids two writers racing over one file, and only the balance *display* makes a network call.
- **Only recently touched logs are opened**, filtered by mtime, so a one-day statement does not pay to decompress the whole history.
## Privacy
Everything stays local. The only network request is the balance query to `api.deepseek.com`, authenticated with your own API key and sent directly from the DSH host process. Statements are written under `$DSH_HOME/token-bill/`. Nothing is uploaded to the plugin author, and the plugin contains no telemetry.
## Testing
```bash
node test/unit.mjs # 21 pure-function tests: money precision, peak hours, zstd frames, ledger rows
node test/report.mjs # 8 report tests: apportionment identity, precision grids, HTML escaping
node test/smoke.mjs # end-to-end against your real ledger, balance endpoint, and session logs
node test/verify.mjs # drives the real execute(), checks output schemas and HTML structure
```
`npm test` runs the three dependency-free suites (`docs`, `unit`, `report`). `smoke` and `verify` read your real DSH data and write statements to the package-local `.dsh-token-bill/` (gitignored), so they need a working DSH home and are best run from a source checkout — `test/` is not part of the published package (`files` ships `lib/`, the patch, the READMEs, and the license).
## Known limitations
- **The ledger depends on the balance widget.** Without it, the money columns become price-table estimates rather than observed charges. The balance itself is always live.
- **Session logs are appended to disk, so the newest turn may not be written yet.** `token_status` is as fresh as the log and the ledger, not instantaneous.
- **Per-hour amounts assume one price table for the whole day.** On a day the provider changes prices, the hourly split is an apportioned share of the observed total, not a per-call reconciliation.
- **The holiday calendar is maintained by hand.** A statutory holiday missing from the list is billed at the peak rate.
- **Only DeepSeek is supported.** The balance source and price tables are DeepSeek-specific; another provider would need its own balance source plus a `models` entry.
## License
[MIT](LICENSE).
Install
dsh plugin --profile web add github:SanChou0627/dsh-token-bill
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-token-bill from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.