Skip to content
dsh.fish
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: 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 <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

  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source