# dsh-usage-dashboard [中文](README.md) | English A DeepSeek Harness (DSH) plugin: shows your **DeepSeek account balance**, **estimated spend for the current conversation**, **per-conversation details (tokens & cost)**, and estimates **remaining turns per model** based on historical average spend. Targets **DSH `>= 0.1.5-rc.1`** (the web and desktop builds share one plugin system). ## Features - **Balance**: calls the official DeepSeek `GET /user/balance` endpoint and shows the account total balance (CNY or USD). - **Current-conversation estimated spend + remaining turns**: below the input box, on its own line after the built-in stats line (`N turns · M steps`), live-updates "estimated spend · balance · estimated remaining rounds" (rounds = ⌊balance ÷ current-conversation average cost per turn⌋). - **Conversation details (global board)**: lists every historical conversation's last-active time, input tokens, output tokens, and cost (newest-activity first, empty/subagent sessions skipped; the current conversation is tagged), with workspace filtering, sort by time/cost, and paging. - **Remaining turns per model**: aggregates historical sessions by model, `remaining = ⌊balance ÷ average cost per session⌋`, plus session count, average cost, total cost, and token details. ## Placement - **Line below the input box** (`conversation.composer.dock`, order 200): current-conversation estimated spend, current balance, estimated remaining rounds; auto-refreshes every 4s while chatting. - **Settings → Usage & Balance board** (`settings.section`): balance card + conversation-details table + per-model remaining turns; auto-refreshes every 15s. ## Install The plugin must be resolvable by the Cordis loader from the profile directory. The DSH desktop (Electron) and web builds share the same plugin system but use different profiles (`desktop` / `web`); install steps are identical — just swap the profile name below (use `web` for the web build). Default Windows paths: - Home (`$DSH_HOME`): `C:\Users\\.dsh` - Desktop profile directory: `C:\Users\\.dsh\profiles\desktop\` - Web profile directory: `C:\Users\\.dsh\profiles\web\` - Plugin resolution: `$DSH_HOME\profiles\\node_modules` (pnpm-managed) + flat fallback `$DSH_HOME\profiles\node_modules` The examples below use `desktop`; **for the web build use `web` instead**. ### Option 1 (recommended): install from GitHub This package is **not published to npm yet**; the current distribution channel is the GitHub repo. Installing means two steps: get it into the profile's `node_modules`, then insert one row in `cordis.patch.yml`. 1. Install from GitHub into the profile's node_modules. **The `name@github:` alias form is required** — plain `github:owner/repo` makes pnpm create a directory named after the *repo* (`dsh-usage-dashboard`), and DSH's resolution by package name then fails: ```bash cd "$DSH_HOME/profiles/desktop" pnpm add '@gongshiyun/dsh-usage-dashboard@github:gongshiyun/dsh-usage-dashboard' # needs git on PATH; a tag works too: ...@github:gongshiyun/dsh-usage-dashboard#v1.1.0 ``` Verify the directory name (it must be `@gongshiyun/dsh-usage-dashboard`, not `dsh-usage-dashboard`): ```bash node -e "console.log(require.resolve('@gongshiyun/dsh-usage-dashboard'))" ``` 2. Edit `$DSH_HOME\profiles\desktop\cordis.patch.yml` and insert the composition row (same as Option 2 step 2), then restart DSH Desktop (web: refresh the page). Verify with `dsh --profile desktop --dump-config` that the `usage-dashboard` entry is active. ### Option 2: manual install (offline / no git) 1. Drop the package files at the **scoped path** (a wrong directory name breaks resolution by package name): ``` $DSH_HOME\profiles\desktop\node_modules\@gongshiyun\dsh-usage-dashboard\ package.json cordis.patch.yml lib\index.js lib\client.js README*.md ``` 2. Edit `$DSH_HOME\profiles\desktop\cordis.patch.yml` and insert a top-level entry (**do not** edit the profile root `cordis.yml` — it is overwritten to `[]` on every launch): ```yaml - insert: - id: usage-dashboard name: '@gongshiyun/dsh-usage-dashboard' ``` Add a `config` block under `name` to override pricing (see below). 3. Restart DSH Desktop (web: refresh the page). > About npm: this package is **not published to npm** (`@gongshiyun/dsh-usage-dashboard` is a 404 on the registry). Once it is, `dsh plugin --profile desktop add @gongshiyun/dsh-usage-dashboard` will do install + composition in one step — the package declares `dsh.bundle.patch`, so `dsh plugin add` appends it to `dsh.profile.bundles` automatically. > > Also note: the unscoped npm package `dsh-usage-dashboard` is a different, unrelated project. > Upgrade note: when a DSH upgrade rebuilds the profile, `cordis.patch.yml` is reset to `[]` and the > plugin directory may be cleaned up (the old `node_modules` is left as `node_modules.dsh-backup-*`). > Re-run the two install/compose steps above after upgrading. ## Configuration | Key | Type | Default | Description | |---|---|---|---| | `apiKeyEnv` | credential-ref | `DEEPSEEK_API_KEY` | credential-ref name for the API key | | `baseURL` | string | `https://api.deepseek.com` | DeepSeek API base URL | | `currency` | `CNY` \| `USD` | `CNY` | pricing & balance display currency (**must match `pricing` units**) | | `balanceCacheMs` | number | `60000` | balance cache TTL (ms) | | `pricing` | array | see below | **price table** (list of eras), each era has an effective time and per-model rates | `pricing` is a **price table** (sorted by effective time; cost uses the era in effect at each model call's event timestamp). **The default already is the official current price list — normally nothing to configure:** ```yaml pricing: - effective: 2026-09-10T04:00:00Z # official current price (Beijing 2026-09-10 12:00) models: - model: deepseek-flash # V4.1 Flash inputPerM: 1 # off-peak: input (cache miss) cacheReadPerM: 0.02 # off-peak: input (cache hit) outputPerM: 4 # off-peak: output peak: # peak-hour rates (only used when offPeak is also present) inputPerM: 2 cacheReadPerM: 0.04 outputPerM: 8 offPeak: # off-peak rates inputPerM: 1 cacheReadPerM: 0.02 outputPerM: 4 ``` - Events before the first era use the first era; models without an entry fall back to that era's `model: "*"` wildcard entry. - **When the provider changes prices, append a new era with a newer `effective`** (or replace the whole list with a single current era). - Legacy format is supported: a flat list of model entries (no `effective`/`models`) is treated as a single era. ### Default prices (CNY per 1M tokens, official current list only) | Effective (UTC) | Model | Period | Input (miss) | Input (hit) | Output | |---|---|---|---|---|---| | 2026-09-10T04:00 | `deepseek-flash` / `deepseek-v4-flash` / `deepseek-v4-flash-vision-exp` | Peak | ¥2 | ¥0.04 | ¥8 | | 2026-09-10T04:00 | same | Off-peak | ¥1 | ¥0.02 | ¥4 | | 2026-09-10T04:00 | `deepseek-v4-pro` | Peak | ¥9 | ¥0.30 | ¥27 | | 2026-09-10T04:00 | `deepseek-v4-pro` | Off-peak | ¥4.5 | ¥0.15 | ¥13.5 | | 2026-09-14T04:00 | all (V4 Pro now routes to V4.1 Flash, billed at Flash prices) | Peak | ¥2 | ¥0.04 | ¥8 | | 2026-09-14T04:00 | all | Off-peak | ¥1 | ¥0.02 | ¥4 | - `deepseek-v4-flash` and `deepseek-v4-flash-vision-exp` are retired model ids; the provider still accepts them but routes to V4.1 Flash and bills at Flash rates, hence the shared row. - **Peak hours are Beijing time Monday–Friday 09:00–12:00 and 14:00–18:00**; every other hour (nights, weekends, and the 12:00–14:00 lunch gap) is off-peak, billed at half the peak price. - The table **deliberately omits the older August 2026 prices**: sessions before 2026-09-10T04:00Z are also priced from the table above, so historical amounts are approximations. Official prices before 8/17 were markedly lower (V4 Pro output ¥6 vs ¥13.5 now), so **older sessions read high**; those sessions are no longer priced exactly. Source: (Chinese: ). Note: the **default rates are baked in and do not auto-follow official price changes** — after another change, add a new era (or wait for a plugin release). The plugin does not crawl the official page. ## Data model & assumptions - **Token accounting**: reads provider `usage` from `assistant/message` events. DSH's `TokenUsage` input counts are **disjoint**: `inputTokens` = cache-miss input, `cacheReadTokens` = cache-hit input, `cacheWriteTokens` = cache write, and billed input is the sum of the three; they correspond to DeepSeek's `prompt_cache_miss_tokens` / `prompt_cache_hit_tokens` / `completion_tokens`. The DeepSeek adapter never emits `cacheWriteTokens` (the provider has no such billing class); the plugin still prices it at the cache-miss rate so other providers' usage is never undercounted. - **Retry dedup**: on the same `(turn, step)`, only the last `assistant/message` (after `llm/retry`) counts — consistent with the token meter. - **Cost formula**: `cost = ((inputTokens + cacheWriteTokens)×inputPerM + cacheReadTokens×cacheReadPerM + outputTokens×outputPerM) / 1e6`. - **Session source**: uses `ctx.sessionQuery` (merges in-memory live sessions + persisted cold sessions); without it, falls back to currently-loaded sessions. Persisted session usage is cached per id and rebuilt on restart. - **Conversation details / latest**: `conversations` is newest-by-**last activity**, only includes user sessions with real token usage (skips empty sessions and `origin === 'subagent'` sessions); the first non-current entry is "latest". - **Remaining turns (model)**: `⌊balance ÷ that model's average cost per session⌋`; `—` when no history or zero average cost. - **Estimated remaining rounds (current)**: `⌊balance ÷ (current-session total ÷ turns)⌋`; `—` when the current session has no completed turns yet. - **Balance amounts are strings**: DeepSeek returns `total_balance` etc. as strings; the plugin converts them to numbers for display and math. ## Architecture (for contributors) - **host** (`lib/index.js`): a Cordis plugin `{ name, inject, apply, Config }` whose core is a `TypertRemoteService` subclass `UsageDashboardGateway` exposing the SRC Remote endpoints `balance()` and `overview(sessionId?)`. SRC mode is derived at runtime from `@Remote` markers by `dsh-api-gateway`, so no Typert-compiler-generated strict descriptors are needed — hence the `installRemote()` helper that manually expands the decorators (plain-JS has no decorator syntax). - **Settings section**: since DSH 0.1.5, `@deepseek-ai/dsh-settings` no longer exports `installSettingsSection()` / `settingsNamespace()` (`SettingsNamespace` is now a pure type), so the plugin uses `ctx.inject(['settings'], ...)` and `ctx.settings.installSection(ctx, ns, Config, base, hooks)`. A `ctx.settings.register(...)` fallback is kept; when neither exists the plugin simply runs on its composition config and balance/usage statistics keep working. - **client** (`lib/client.js`): the `window.__ModuleLoader__.load({ id, factory })` form, registering React components via `ctx.slots.inject(...)` into `settings.section` and `conversation.composer.dock`; data is fetched via `ctx.connection.rpc.call('/api', 'usageDashboard/', { args })`. Both registrations declare `locale: 'usage-dashboard'`, so copy follows the active language. ## Limitations - Balance is polled on demand (composer 4s, board 15s), not pushed. - With no network or no configured API key, balance shows an error; cost stats still work (from local session data). - Cross-session history averaging depends on the `dsh-session-query` service (included in the default DSH composition).