# dsh-usage-stats

> Provides multi-vendor account balances, Token Plan quotas, and daily token usage heatmaps for the DSH web interface, displayed in a unified sidebar floating panel.

## Metadata

- Author: [@Ychris12138](https://github.com/Ychris12138)
- Repo: <https://github.com/Ychris12138/dsh-usage-stats.git>
- GitHub: [Ychris12138/dsh-usage-stats](https://github.com/Ychris12138/dsh-usage-stats)
- Stars: 96
- Language: JavaScript
- License: [MIT](https://spdx.org/licenses/MIT.html)
- Topics: `deepseek`, `deepseek-harness`, `deepseek-harness-plugin`, `deepseek-harness-plugin-dev`, `deepseek-harness-plugins`, `dsh`, `dsh-plugin`, `dsh-plugins`
- Forks: 15
- Open Issues: 6
- Last push: 2026-08-20T12:07:56.000Z
- Added: 2026-08-17T00:00:00.000Z

## Install

```bash
dsh plugin --profile web add github:Ychris12138/dsh-usage-stats
```

## Wiki

## One-Line Description
Complements DeepSeek Harness web UI with multi-vendor account balances, subscription/Token Plan quota windows, and daily token usage and cache hit rates aggregated by provider/model—all consolidated in a floating panel at the bottom of the sidebar. Regular users in `dsh web` can immediately see how many tokens they've burned today and how long their account will last, without switching to each vendor's own console.

## Core Features
- New "Usage/Balance" entry in sidebar bottom: Opens a floating panel showing only the currently selected provider, with a manual refresh button in the title bar (simultaneously refreshes tokens, provider list, and current account)
- Daily Token usage statistics: Today, this month, cumulative, cache hit rate, drill-down by date/provider/model; same model from different providers are grouped separately (e.g., `deepseek-official · deepseek-chat` vs `ark · deepseek-chat`)
- Code-like blue calendar heatmap: Cells mapped continuously using square root of monthly max (zero is neutral gray "empty"), clicking a date expands provider/model details with hit rate bars
- Multi-vendor balance unified cards: DeepSeek, OpenRouter, Moonshot, Z.ai, New API, Sub2API, Passion, general balance template, colored by remaining ratio "normal/warning/critical" (default 30%/10% thresholds)
- Subscription / Token Plan quota windows: OpenCode Go rolling usage, Z.ai Coding Plan (China region bigmodel-cn supported), Kimi For Coding, MiniMax Coding Plan (cn region supported), each showing remaining ratio and reset time
- Backend automatic monitoring: Server refreshes once on startup, then every 5 minutes refreshes all configured accounts and local token aggregation; browser only requests current provider, refresh frequency independent of panel open state

## Technical Implementation
- **Language**: JavaScript (ESM; 5 `.js` files under `lib/`, client bundle single file 1464 lines hand-written, no build step)
- **Key Dependencies**: Pure Node.js built-ins—`node:fs/promises` (atomic cache writes), `node:http`/`node:https`/`node:dns`/`node:net` (upstream requests + DNS resolution + IP filtering), `node:path`/`node:os` (DSH_HOME resolution). Client uses host bundle's React 18 and `@deepseek-ai/dsh-client-ui-primitives`
- **Architecture Pattern**: dual-half host plugin. `cordis.patch.yml` inserts 1 line Loader in web profile (`id: usage-stats, name: dsh-usage-stats`); `package.json#dsh.bundle.patch` points to patch; server `apply(ctx, rawConfig)` registers 5 exact GET routes and starts 5-minute timer; client injects `sidebar.footer.action` slot via `__ModuleLoader__`, uses `React.createPortal` to mount floating layer to `document.body` avoiding sidebar theme variable leakage
- **Entry Files**: Server `lib/index.js` (`apply` / `name = "usage-stats"` / `Config = { ~standard: ... }`); client `lib/client.js`; account adapter `lib/accounts.js`; token aggregation `lib/usage.js`; balance schemes `lib/balance.js`; subscription windows `lib/subscriptions.js`

## Use Cases
DSH users who want to centralize "how much money left / how much quota / how many tokens burned today" across multiple LLM vendors, especially developers using DeepSeek official + several Coding Plans + self-hosted New API/Sub2API proxies. This plugin doesn't display models or conversation history—it only adds an independent floating panel to the existing sidebar, zero workflow intrusion.

## Prerequisites & Compatibility

| Dependency | Min Version | Notes |
|---|---|---|
| DeepSeek Harness (DSH) | `>=0.1.0-rc.6` | README explicitly requires `@deepseek-ai/dsh >= 0.1.0-rc.6`, uses `web` profile |
| Node.js | Not declared | No `engines` field in `package.json`; scripts use `node:fs/promises`, `node:dns/promises` etc., installer is single-file `.mjs` |
| Platform | Cross-platform | All pure JS, no native modules; cache via `node:fs` atomic writes, DNS resolution and HTTP requests use built-in modules |
| Native Modules | None | Only third-party dependency is `react`/`react-dom` devDependency, no runtime `*.node` |
| Network | Upstream account interface access required (optional) | Balance endpoints by provider: DeepSeek `api.deepseek.com`, OpenRouter `openrouter.ai`, OpenCode Go `opencode.ai`, Z.ai `api.z.ai` or `open.bigmodel.cn`, Kimi `api.kimi.com`, MiniMax `www.minimax.io`/`www.minimaxi.com`; local token aggregation doesn't need external network |

## Installation
```bash
dsh plugin --profile web add github:Ychris12138/dsh-usage-stats
```
Or via npx compatible install: `npx --yes github:Ychris12138/dsh-usage-stats`, supports `--dry-run` / `--check` / `--no-enable`. After install or update, must restart running `dsh web` and hard-refresh browser to load new client bundle.

## Configuration Options
Plugin works out of the box—no config needed to see Token usage heatmap in sidebar; all fields below are optional, only needed when you want the panel to show a specific provider's balance/Token Plan. Write config under `config.monitors` of existing `name: dsh-usage-stats` entry in `~/.dsh/profiles/web/cordis.patch.yml` (don't create a second plugin entry, otherwise will mount twice).

| Config | Type | Description | Default |
|---|---|---|---|
| `monitors.<providerId>` | object | Enable balance/subscription monitoring for a provider; key must be real provider id in Harness, unknown provider/adapter/illegal mapping blocks plugin startup before route registration | Not filled = only show Token heatmap |
| `monitors.<providerId>.adapter` | enum | Adapter: `deepseek-balance` / `openrouter-balance` / `moonshot-balance` / `zai-balance` / `new-api` / `sub2api` / `general` / `opencode-go` / `zai-token-plan` / `kimi-token-plan` / `minimax-token-plan` / `declarative`. Not filled = auto-detect by baseURL domain (passionapi.com → sub2api), rest use built-in balance scheme | Auto-detect by domain |
| `monitors.<providerId>.credentialRef` | string | Credential reference name (e.g., `OPENROUTER_MANAGEMENT_KEY`); plugin doesn't store Key value, only resolves via Harness credentials seam at request time | Balance-type adapter uses corresponding provider's `apiKeyEnv` |
| `monitors.<providerId>.usageBaseURL` | URL | Override provider's account interface base address; no embedded username/password allowed, defaults to HTTPS | Provider's configured baseURL |
| `monitors.<providerId>.allowInsecure` | boolean | Allow `usageBaseURL` to use `http://` (only when you control the intranet and explicitly agree) | `false` |
| `monitors.<providerId>.fallbackCredentialRef` / `.fallbackUserIdRef` | string | New API falls back to `/api/user/self` admin PAT and User ID when `/api/usage/token/` returns 404/405 (won't use inference token as admin credential) | Fallback not enabled |
| `monitors.<providerId>.warning.warnBelow` / `.warning.criticalBelow` | number | Absolute balance thresholds; remaining ≤ criticalBelow = red, ≤ warnBelow = yellow (criticalBelow must ≤ warnBelow). Balances with total quota / Token Plan also auto-colored by 30%/10% ratio | Custom threshold not enabled |
| `monitors.<providerId>.mode` | `"balance"` / `"subscription"` | Only needed for declarative adapter, specifies parsed view type | Required |
| `monitors.<providerId>.request.path` | string | Same-origin relative path (e.g., `/account/balance`), no absolute URL or protocol-relative path; method only accepts GET; body max 1 MiB | Required |
| `monitors.<providerId>.request.auth.type` | `"bearer"` / `"raw"` / `"x-api-key"` | Credential injection method, credentialRef points to variable name in `~/.dsh/.credentials.yaml`; sensitive headers like `Authorization`/`X-API-Key`/`API-Key` cannot be directly overridden in `request.headers`, must be injected via credential reference | bearer |
| `monitors.<providerId>.request.headers` | object | Extra HTTP headers (only non-sensitive headers); plugin defaults to manual redirect, JSON-only response, 1 MiB body limit | — |
| `monitors.<providerId>.extract.<field>` | JSON Pointer | Extract fields from upstream response (`root` / `remaining` / `used` / `total` / `currency` / `items` / `kind` / `usedPercent` / `remainingPercent` / `resetsAt` …); only performs restricted GET + JSON Pointer, no JavaScript execution; `items` required in subscription mode | Built-in per adapter |
| `monitors.<providerId>.extract.divisor` | number | Divide corresponding field by this number before display (e.g., convert "cents" to "dollars"); must be non-zero | — |

## FAQ

**Q: Don't see "Usage/Balance" entry in sidebar after install. What to do?**

A: After installing or updating plugin, must restart running `dsh web` and hard-refresh browser (Ctrl/Cmd+Shift+R). Plugin's server routes and client bundle are both mounted when `dsh web` starts; only refreshing browser won't reload. Installer writes one line `- insert: id: usage-stats, name: dsh-usage-stats` to `cordis.patch.yml`, repeated runs are idempotent, won't append duplicates.

**Q: Works without configuring Keys? What can I see?**

A: Token usage heatmap and cache hit rate work completely without any credentials, automatically aggregating all session event streams. Providers without public balance endpoints show "Not Supported" in account cards—plugin won't guess; providers missing Keys show "Unconfigured". OpenRouter is the exception: official account credits must use separate `OPENROUTER_MANAGEMENT_KEY`, plugin won't try inference `OPENROUTER_API_KEY` against `/api/v1/key`.

**Q: Where is data stored? Does it upload?**

A: Server aggregation cache only saves token counts folded by provider/model, session IDs, opaque revisions and fold cursors, written to `$DSH_HOME/storages/usage-stats-cache.json` using temp file + atomic `rename`. API Keys, OpenCode `auth.json`, Cookies, admin PATs and raw upstream responses never enter browser response, plugin cache or logs. The `validate:live` script compares plugin endpoint vs official token projection per session, returns non-zero exit code on mismatch—but script may output real balance, don't paste output to public issue.

**Q: Can I connect my own private vendor?**

A: Yes, using `declarative` adapter: set `mode: balance` or `mode: subscription`, `request.path` (HTTPS same-origin relative path), `request.auth.type` (bearer/raw/x-api-key), `extract.*` JSON Pointer fields. Only restricted GET + JSON Pointer execution, no JavaScript. Credentials can only be injected via reference; `usageBaseURL` doesn't allow embedded username/password; sensitive headers (`Authorization`/`X-API-Key`/`API-Key`/`Cookie` etc.) must be rejected by plugin if directly overridden in `request.headers`. Before sending credentials, plugin filters domain's IPv4/IPv6 resolution results to a single allowed connection address, prioritizing public IPs.

**Q: OpenCode Go quota suddenly unavailable / interface returns 401. What to do?**

A: OpenCode Go's Bearer usage endpoint (`opencode.ai/zen/go/v1/usage`) is not an official public API; upstream may change structure anytime; panel shows specific error when interface changes. Can explicitly set `credentialRef: OPENCODE_GO_API_KEY` under `monitors.opencode-go`, or fall back to `OPENCODE_GO_AUTH_COOKIE + OPENCODE_GO_WORKSPACE_ID` cookie-compatible solution (plugin prioritizes Harness credential, then reads `~/.local/share/opencode/auth.json`, finally uses cookie). To disable, simply delete corresponding monitor from Cordis entry.

**Q: Can reverse proxy expose these five endpoints to public network?**

A: Not recommended. The five endpoints (`/api/usage-stats/{usage,providers,balance,subscriptions,account}`) only verify peer socket is loopback + Host header, no identity authentication; if behind reverse proxy, plugin sees proxy's own loopback address as peer, equivalent to bypassing security boundary. README and SECURITY.md both explicitly state "do not expose endpoints via reverse proxy to LAN or public networks; if proxying is necessary, must add reliable authentication and access control at proxy layer". Security issues please report privately per SECURITY.md.

**Q: How to uninstall?**

A: Just run `dsh plugin --profile web remove dsh-usage-stats` and restart `dsh web`. Cache file `$DSH_HOME/storages/usage-stats-cache.json` left on disk, manually delete if needed; cordis patch written by installer also cleaned up. Installer won't touch `~/.dsh/.credentials.yaml`.

## Getting Started
Beginner—one `dsh plugin --profile web add` + restart `dsh web` shows "Usage/Balance" entry in sidebar, token usage stats automatically available. Advanced part is reading README's `monitors.<providerId>` YAML config examples, configuring corresponding adapter and credential reference for OpenRouter, OpenCode Go, various Coding Plans and self-hosted proxies (New API / Sub2API / Passion / declarative).

## Known Issues & Limitations
- OpenCode Go's Bearer usage endpoint (`opencode.ai/zen/go/v1/usage`) is non-official public API: when upstream changes structure, quota panel shows error; can use `OPENCODE_GO_AUTH_COOKIE + OPENCODE_GO_WORKSPACE_ID` cookie fallback, or directly delete monitor to disable
- DSH client sidebar layout is ROW arrangement: this plugin as second footer action triggers host layout to push subsequent items out of visible area—plugin works around by temporarily switching host container to column layout (see `flexDirection` toggle effect in client.js, issue #21 recorded)
- Some third-party Coding Plans have balance only: Kimi Code 5 hours/week window, Z.ai partial interfaces need adapter updates when upstream structure changes; Anthropic OAuth, OpenAI Codex, Gemini Code Assist, GitHub Copilot Personal have no API-keyable usage endpoint yet
- DSH restarts with compressed summary to restore active sessions: when plugin detects in-memory event cursor below fold cursor, it re-folds entire session statistics (issue #23 recorded), usage numbers briefly "rollback" then catch up after restart
- Account cards portal to `document.body` within panel container: avoids sidebar theme variable leaking into floating layer (issue #17 recorded), but therefore visually completely independent from sidebar
- Custom monitor defaults require HTTPS + same-origin relative path + manual redirect + JSON + 1 MiB limit: `allowInsecure` must be explicitly enabled to use HTTP; `198.18.0.0/15` range only accepted as Clash/Mihomo-style fake-IP synthesis address (HTTPS only), literal IPs and other private addresses rejected by default, prevents DNS rebinding bypass
- Must restart `dsh web` after install/update: server exact routes, 5-minute timer and client bundle all mount when `dsh web` starts, browser refresh alone won't reload
- `dsh plugin` and `npx` are two independent install paths: don't keep both manual cordis entry and bundle registration, otherwise will mount twice; installer checks YAML root node is not empty `[]` before writing, residual `[]` from historical versions auto-cleaned

---

This document is auto-generated by [deepseek-plugin.org](https://deepseek-plugin.org). HTML page: [dsh-usage-stats](https://deepseek-plugin.org/plugins/Ychris12138/dsh-usage-stats)
Wiki generated by AI (model: `MiniMax-M2.5`)
