# dsh-deepseek-usage-monitor English | [简体中文](README.zh-CN.md) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) [![test](https://github.com/KamChiHei/dsh-usage-monitor/actions/workflows/test.yml/badge.svg)](https://github.com/KamChiHei/dsh-usage-monitor/actions/workflows/test.yml) [![npm version](https://img.shields.io/npm/v/dsh-deepseek-usage-monitor)](https://www.npmjs.com/package/dsh-deepseek-usage-monitor) [![npm downloads](https://img.shields.io/npm/dm/dsh-deepseek-usage-monitor)](https://www.npmjs.com/package/dsh-deepseek-usage-monitor) [![GitHub stars](https://img.shields.io/github/stars/KamChiHei/dsh-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): ![Expanded "Usage" status card in the bottom-right corner of DSH Web](docs/screenshot.png) ## 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 ``` 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\\.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) |