dsh-usage-stats

96Stars15Forks6Issues0Watchers

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.

Language
JavaScript
License
MIT
Branch
main
deepseekdeepseek-harnessdeepseek-harness-plugindeepseek-harness-plugin-devdeepseek-harness-pluginsdshdsh-plugindsh-plugins

Install

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

Run the command above in your terminal to install this plugin via the dsh CLI. You can switch Profile in the top-right corner. New to dsh? Read the beginner tutorial

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

DependencyMin VersionNotes
DeepSeek Harness (DSH)>=0.1.0-rc.6README explicitly requires @deepseek-ai/dsh >= 0.1.0-rc.6, uses web profile
Node.jsNot declaredNo engines field in package.json; scripts use node:fs/promises, node:dns/promises etc., installer is single-file .mjs
PlatformCross-platformAll pure JS, no native modules; cache via node:fs atomic writes, DNS resolution and HTTP requests use built-in modules
Native ModulesNoneOnly third-party dependency is react/react-dom devDependency, no runtime *.node
NetworkUpstream 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

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).

ConfigTypeDescriptionDefault
monitors.<providerId>objectEnable balance/subscription monitoring for a provider; key must be real provider id in Harness, unknown provider/adapter/illegal mapping blocks plugin startup before route registrationNot filled = only show Token heatmap
monitors.<providerId>.adapterenumAdapter: 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 schemeAuto-detect by domain
monitors.<providerId>.credentialRefstringCredential reference name (e.g., OPENROUTER_MANAGEMENT_KEY); plugin doesn't store Key value, only resolves via Harness credentials seam at request timeBalance-type adapter uses corresponding provider's apiKeyEnv
monitors.<providerId>.usageBaseURLURLOverride provider's account interface base address; no embedded username/password allowed, defaults to HTTPSProvider's configured baseURL
monitors.<providerId>.allowInsecurebooleanAllow usageBaseURL to use http:// (only when you control the intranet and explicitly agree)false
monitors.<providerId>.fallbackCredentialRef / .fallbackUserIdRefstringNew 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.criticalBelownumberAbsolute balance thresholds; remaining ≤ criticalBelow = red, ≤ warnBelow = yellow (criticalBelow must ≤ warnBelow). Balances with total quota / Token Plan also auto-colored by 30%/10% ratioCustom threshold not enabled
monitors.<providerId>.mode"balance" / "subscription"Only needed for declarative adapter, specifies parsed view typeRequired
monitors.<providerId>.request.pathstringSame-origin relative path (e.g., /account/balance), no absolute URL or protocol-relative path; method only accepts GET; body max 1 MiBRequired
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 referencebearer
monitors.<providerId>.request.headersobjectExtra HTTP headers (only non-sensitive headers); plugin defaults to manual redirect, JSON-only response, 1 MiB body limit
monitors.<providerId>.extract.<field>JSON PointerExtract 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 modeBuilt-in per adapter
monitors.<providerId>.extract.divisornumberDivide 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