Bundle
dsh-deepseek-usage-monitor
DeepSeek Harness plugin for token usage and account balance monitoring.
- Source
- KamChiHei
- License
- MIT
- Updated
- Updated 6 days ago
Readme
# dsh-deepseek-usage-monitor
English | [简体中文](README.zh-CN.md)
[](LICENSE)
[](https://github.com/KamChiHei/dsh-usage-monitor/actions/workflows/test.yml)
[](https://www.npmjs.com/package/dsh-deepseek-usage-monitor)
[](https://www.npmjs.com/package/dsh-deepseek-usage-monitor)
[](https://github.com/KamChiHei/dsh-usage-monitor/stargazers)
A DeepSeek Harness (`dsh`) plugin: records token usage for every model call on the Host side, polls your DeepSeek account balance on a timer, and shows a draggable, resizable live status card in the bottom-right corner of DSH Web.
The plugin has two halves reading the same data:
- **Host side** (`index.js`): listens to harness events for accounting, queries the balance on a timer, and serves the state endpoint;
- **Web side** (`client.js`, loaded via the `dsh.client` declaration in `package.json`): polls the state endpoint and renders the bottom-right "Usage" card. The API key never leaves the Host process and is never sent to the browser.
## Showcase
The "Usage" card in the bottom-right corner of DSH Web after installation (shown expanded, with total tokens, cache hit rate, balance, and model / provider groups):

## Features
### Token accounting
- Listens to `session/event`: `assistant/message` `TokenUsage` is the source of truth; usage recorded by `assistant/chunk` (`chunk.type === "usage"`) is the fallback for failed requests, deduplicated by the `session:turn:step` key so a step is never counted twice; `step/end` and `session/disposed` flush chunk usage that never received a message confirmation.
- Accepts both usage shapes: harness `inputTokens / outputTokens / cacheReadTokens / cacheWriteTokens`, and raw DeepSeek response fields `prompt_tokens / prompt_cache_hit_tokens / prompt_cache_miss_tokens / completion_tokens ...` (converted automatically; miss = prompt − hit when omitted).
- `totalTokens = input + output + cache read + cache write`; reasoning tokens are already included in output — tracked separately but never double-counted.
- Besides the grand totals, usage is grouped by **model** and **provider**; per-session details keep the most recent `sessionLimit` entries (`sessionCount` is the number of retained sessions). Routing info comes from `request/header` / `request/context` events; missing names fall into the `unknown` group.
- Stats are persisted to a local JSON file (default `~/.deepseek-harness/deepseek-usage.json`) and keep accumulating across restarts. Only numbers, group names, and timestamps are stored — never API keys, prompts, or model replies. Delete the file and restart DSH to reset the stats.
### Balance queries
- Polls the official DeepSeek `GET /user/balance` endpoint on a timer (default 60s), recording `is_available` and `balance_infos` amounts; timeouts (default 10s) and failures are recorded with the reason.
- The API key is **resolved per refresh** and automatically reuses the DeepSeek key already configured in dsh (resolution order under "API key"); a key added after startup takes effect on the next balance refresh — no restart needed.
- Background refreshes silently skip when no key resolves (the card shows "Not queried"); only a manual refresh marks "Query failed" — hover the balance row to see the reason (including diagnostics for a missing key). Token accounting never depends on the key and always works.
### State endpoint
`GET /plugins/deepseek-usage-monitor/state`: the endpoint used by the web card; add `?refresh=1` to force a balance refresh; `HEAD` is supported. See "State endpoint response" below for the shape.
### DSH Web status card
After installation a "Usage" card appears in the bottom-right corner of DSH Web and polls state every 5 seconds (polling pauses while the tab is hidden and refreshes once immediately when it becomes visible again):
- Expanded view: total tokens, request count, cache hit rate, input (cache-miss), output tokens, DeepSeek API balance, model / provider group lists, and last-updated time; click "Refresh" to force a balance refresh immediately (equivalent to `?refresh=1`).
- Cache hit rate = cache read / (cache read + cache-miss input).
- Model / provider groups are sorted by total tokens descending; only the top 4 entries are shown by default — click "Show all N" to expand and "Collapse" to fold; "No data" when empty.
- Balance row states: Reading… / amounts (multiple currencies joined with `·`) / No balance / Unavailable / Not queried / Query failed (hover for the reason).
- Collapses to a title bar by default; click "+" to expand and "−" to collapse — the state is remembered.
- Drag the title bar to move, drag the bottom-right handle to resize (minimum 232×96), double-click the title bar to reset to the default bottom-right anchor; position, size, and collapsed state persist in browser localStorage (key `dsh-deepseek-usage-monitor:placement`) across page reloads.
- Collapsing hides the resize handle and returns the bar to its docking spot; the card pulls itself back into the viewport when expanded near an edge or when the window shrinks.
- The status dot turns red when the state endpoint fails; the error message shows at the bottom of the card.
- Styling is built on the official DSH design tokens (`--dsw-*` for backgrounds, borders, text levels, and state colors; `--ds-*` for motion) with fallbacks, adapting to dark/light themes automatically; responsive width on small screens (≤560px).
- The card UI language follows the browser locale: Chinese for `zh-*` locales, English otherwise.
## Requirements
- Node.js ≥ 22.19
- pnpm (`dsh plugin` essentially forwards pnpm inside the profile directory)
- No global `dsh` install needed: every `dsh` command can run via `pnpm dlx`. This document uses:
```powershell
pnpm dlx @deepseek-ai/dsh@0.1.1-rc.2 <command>
```
Replace `0.1.1-rc.2` with the dsh version you actually use (the `package.json` scripts are written the same way).
## Install into a profile
Harness configuration and profiles live under `~/.dsh` (`C:\Users\<you>\.dsh` on Windows); the web profile is `~/.dsh/profiles/web`. `dsh plugin` forwards pnpm in that directory and automatically adds dependencies that declare `dsh.bundle` to the profile's bundle layer — no YAML editing needed.
### Option 1: npm install (recommended, stable releases)
No cloning and no manual dependency installs — run from any directory:
```powershell
pnpm dlx @deepseek-ai/dsh@0.1.1-rc.2 plugin --profile web add dsh-deepseek-usage-monitor
```
- Plugin dependencies (`@deepseek-ai/schemastery` etc.) are installed into the profile's own `node_modules`, and the plugin joins `dsh.profile.bundles` automatically;
- Update to the latest release: run the same command again;
- Pin a version: `plugin --profile web add dsh-deepseek-usage-monitor@0.1.0`.
### Option 2: GitHub direct install (tracks the latest commit)
The install source points straight at the GitHub repository, so you always get the latest main-branch code:
```powershell
pnpm dlx @deepseek-ai/dsh@0.1.1-rc.2 plugin --profile web add github:KamChiHei/dsh-usage-monitor
```
- `~/.dsh/profiles/web/package.json` gains `"dsh-deepseek-usage-monitor": "git+https://github.com/KamChiHei/dsh-usage-monitor.git"` and the plugin joins `dsh.profile.bundles`;
- Update to the latest commit: run the same command again;
- Pin a version: use a tag reference such as `github:KamChiHei/dsh-usage-monitor#v0.1.0`.
### Option 3: Local link install (for hacking on the source)
Run two steps inside the plugin directory:
```powershell
cd C:\path\to\dsh-usage-monitor
# 1. Install the plugin's own dependencies (required first, see below)
pnpm install
# 2. Register the plugin into the web profile
pnpm dlx @deepseek-ai/dsh@0.1.1-rc.2 plugin --profile web add .
```
Or use the bundled one-liner (run inside the plugin directory, same effect as step 2 above):
```powershell
pnpm run install:web
```
**Why `pnpm install` first**: pnpm registers the local directory as a `link:` dependency (a symlink) and does not install `@deepseek-ai/schemastery` and other dependencies for the plugin directory; Node resolves modules from the plugin's real path without going through the profile's `node_modules`, so the plugin directory needs its own `node_modules`.
Once installed:
- `~/.dsh/profiles/web/package.json` gains `"dsh-deepseek-usage-monitor": "link:C:/path/to/dsh-usage-monitor"` and the plugin joins `dsh.profile.bundles`;
- Because `link:` is a live link, source changes take effect after **restarting DSH** — no reinstall needed.
### Launch and verify
Launch (as usual):
```powershell
pnpm dlx @deepseek-ai/dsh@0.1.1-rc.2 web
```
The Host startup log should show `[deepseek-usage-monitor] loaded; web: /plugins/deepseek-usage-monitor/state`, and the "Usage" card appears in the bottom-right corner.
Check that the plugin layer entered the composed config tree:
```powershell
pnpm dlx @deepseek-ai/dsh@0.1.1-rc.2 --profile web --dump-config
```
The output should contain a `# == dsh-deepseek-usage-monitor` layer.
For other profiles, replace `web` with the profile name.
### Fixing node_modules after moving the plugin directory
pnpm creates **absolute-path symlinks** under `node_modules/@deepseek-ai/`. Move or rename the plugin directory and they all dangle — dsh fails at startup with `Cannot find package '@deepseek-ai/schemastery'`, and a plain `pnpm install` (Already up to date) won't fix it. In that case, inside the plugin directory run:
```powershell
Remove-Item -Recurse -Force node_modules
pnpm install
```
### Uninstall
```powershell
pnpm run uninstall:web
# or
pnpm dlx @deepseek-ai/dsh@0.1.1-rc.2 plugin --profile web remove dsh-deepseek-usage-monitor
```
## Debugging from local source
The official tutorial's `--patch` flow requires an absolute path to the plugin entry. This repository ships the template `cordis.local.patch.yml` with a hardcoded absolute path to `index.js` — after cloning or moving the repo, change it to your local path first.
Run from any directory (usually the plugin directory itself):
```powershell
pnpm dlx @deepseek-ai/dsh@0.1.1-rc.2 web --patch "C:\path\to\dsh-usage-monitor\cordis.local.patch.yml"
```
Or use the one-liner inside the plugin directory (relative paths resolve against the current directory):
```powershell
pnpm run dev:web
```
`--patch` loads the source entry and likewise requires a prior `pnpm install` in the plugin directory.
## API key
Balance queries need a DeepSeek API key, but **usually no extra configuration is required**: the plugin reuses the key already configured in dsh — the credential store written by the web Models page (`~/.dsh/.credentials.yaml`). If you can chat with DeepSeek models in dsh, balance queries just work.
The key resolves in this order, stopping at the first hit:
1. Plugin config `apiKey` (see the table below);
2. The `DEEPSEEK_API_KEY` environment variable in the launching terminal (`$env:DEEPSEEK_API_KEY = "sk-..."` before starting `dsh web`; both of these are fixed at startup);
3. The dsh credential service (`ctx.get("credentials")`, **re-resolved on every refresh**), layering: process environment → Models-page credential store → project `.env` → `~/.dsh/.env`.
A key added on the Models page after startup takes effect on the next balance refresh (interval `balanceRefreshMs`) without a restart; changing the first two (config and launch-time env) requires a restart.
## Configuration
Overrides go in the profile's `cordis.patch.yml` (`~/.dsh/profiles/web/cordis.patch.yml`). DSH patches replace whole lines, so keep `name` when overriding:
```yaml
- replace:
- id: deepseek-usage-monitor
name: dsh-deepseek-usage-monitor
config:
balanceRefreshMs: 60000
requestTimeoutMs: 10000
recentLimit: 200
```
Options:
| Option | Default | Description |
| --- | ---: | --- |
| `apiKey` | `""` (empty) | Explicit DeepSeek API key; takes precedence over the environment variable and the dsh credential store; leave empty to reuse the key configured in dsh |
| `baseUrl` | `https://api.deepseek.com` | DeepSeek API base URL (trailing slashes stripped) |
| `storePath` | `~/.deepseek-harness/deepseek-usage.json` | Stats file path (`~` expansion supported) |
| `balanceRefreshMs` | `60000` | Balance refresh interval in ms (clamped to ≥ 5000) |
| `requestTimeoutMs` | `10000` | Balance request timeout in ms (clamped to ≥ 1000) |
| `recentLimit` | `100` | Recent calls retained and returned by the state endpoint (≥ 1) |
| `sessionLimit` | `50` | Recent sessions retained by last-request time (≥ 1) |
## Usage
After installing and restarting DSH Web, the "Usage" card in the bottom-right corner works automatically — no conversation needed; see "DSH Web status card" above for the interactions. Click "Refresh" to force a balance refresh immediately (equivalent to `?refresh=1`).
The plugin listens to `session/event` in the background: `assistant/chunk` usage is the fallback source for failed requests; successful requests are merged with `assistant/message.usage` into a single record, avoiding double counting. Reasoning tokens are already included in output tokens and never added twice.
### State endpoint response
`GET /plugins/deepseek-usage-monitor/state` returns:
```json
{
"generatedAt": "2026-08-22T00:00:00.000Z",
"totals": {
"requests": 15,
"inputTokens": 21000,
"outputTokens": 8000,
"cacheReadTokens": 15000,
"cacheWriteTokens": 1200,
"reasoningTokens": 4000,
"totalTokens": 45200,
"lastRequestAt": "2026-08-22T00:00:00.000Z"
},
"sessionCount": 2,
"models": [
{ "key": "deepseek-chat", "totals": { "requests": 12, "totalTokens": 45678 } },
{ "key": "deepseek-reasoner", "totals": { "requests": 3, "totalTokens": 12345 } }
],
"providers": [
{ "key": "deepseek", "totals": { "requests": 15, "totalTokens": 58023 } }
],
"balance": {
"checkedAt": "2026-08-22T00:00:00.000Z",
"isAvailable": true,
"balanceInfos": [{ "currency": "CNY", "total_balance": "110.00" }]
},
"recent": [{ "timestamp": "…", "sessionId": "…", "turn": 1, "step": 1, "provider": "deepseek", "model": "deepseek-chat", "usage": { "…": "…" } }]
}
```
Notes:
- `models` / `providers` are sorted by total tokens descending (name order breaks ties); `recent` is newest-first with at most `recentLimit` entries; `sessionCount` is the number of retained recent sessions (capped at `sessionLimit`);
- a failed balance query adds an `error` field inside `balance` (with the reason) and sets `isAvailable` to `false`;
- missing group names fall into `unknown`; store files from older versions without group data start from empty groups — no migration needed.
## Verification and tests
No real API key needed. Pure-function tests cover usage normalization, reasoning deduplication, group keys, group accumulation, sorting, session pruning, and store-path expansion; integration tests cover `UsageLedger` accounting dedup, failed-request fallback, persistence round-trip, legacy store migration, write-failure recovery, and balance refresh (keys are stubbed):
```powershell
pnpm test
```
Syntax check:
```powershell
node --check index.js
```
The balance shape follows DeepSeek's official `is_available` / `balance_infos` response; token shapes follow the harness `TokenUsage` spec and DeepSeek's prompt-cache fields.
## Project structure
| File | Purpose |
| --- | --- |
| `index.js` | Host entry: event accounting, balance refresh, and the state endpoint |
| `client.js` | Web entry: bottom-right status card UI and polling |
| `usage-utils.mjs` | Pure functions: usage normalization, accumulation, grouping, sorting, session pruning, and store-path expansion (independently testable) |
| `cordis.patch.yml` | Insertion entry declared via `dsh.bundle` when installing into a profile |
| `cordis.local.patch.yml` | `--patch` source-debugging template (contains a hardcoded absolute path — edit after cloning) |
| `tests/usage-utils.test.mjs` | Pure-function tests (`node --test`) |
| `tests/usage-ledger.test.mjs` | `UsageLedger` integration tests: accounting dedup, persistence, balance refresh (`node --test`, no real key needed) |
Install
dsh plugin --profile web add github:KamChiHei/dsh-deepseek-usage-monitor
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-deepseek-usage-monitor from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.