# dsh-billing **A billing plugin for DeepSeek Harness** (bilingual UI: 简体中文 / English) Persistent ledger · exact per-call billing · official balance · budget · today/month/total summaries · per-session details & history · peak/off-peak pricing · official price sync [![version](https://img.shields.io/badge/version-1.0.1-4176E6)](package.json) [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE) [![dsh](https://img.shields.io/badge/DeepSeek%20Harness-dsh--plugin-4176E6)](https://github.com/deepseek-ai/deepseek-harness) **English** | [中文](README.md) --- ## Features | Feature | Location | Notes | |---|---|---| | Session cost | Conversation stats line | Live token totals (input/cache/output) + estimated cost at the current price tier | | Today's cost | Stats line / sidebar | Exact value from the ledger | | Official balance | Stats line / sidebar footer / settings | Total / granted / topped-up; auto-refresh + manual refresh (click forces a fresh query and shows the last-updated time) | | Summary cards | Settings | Today / month / total cost, call counts and token details | | Budget | Settings + sidebar progress bar | Amount, period (today/month/all/custom range), used % (≥80% warning, ≥100% over) | | Today's sessions | Settings | Per-session call counts, input/cache/output tokens and cost | | History | Settings | Daily aggregates, pruned by retention days (default 180, fixed) | | Price table | Settings | Per-model base / off-peak / peak tiers, **read-only**: auto-synced from the official page at every startup, official models only, priced in the display currency (CNY by default) | | Peak/off-peak pricing | Settings | **Read-only**: peak/off-peak hours and whether it is currently peak or off-peak (times synced from the official platform) | | Official price sync | Settings | Fetch and parse the official pricing page, apply with one click; also runs automatically at startup | | Data | Settings | Clear all history | ## Billing rules - Prices are **USD / 1M tokens**, matching the official docs; - Cost = cache-miss input × `cacheMiss` + output × `output` + (cache read + cache write) × `cacheHit`; - **Peak/off-peak is time-gated**: before `peakEffectiveAt` (default `2026-08-16T16:00:00Z`) the base price applies; afterwards peak hours (01:00–04:00 and 06:00–10:00 UTC) use the peak tier and the rest use the off-peak tier; - The ledger always stores **USD**; the display currency is fixed to the defaults (CNY, ¥, rate 7.2, 4 decimals) — no UI to change it; - **Every model call is billed from its `usage` block** (the plugin listens at the tail of the `llm/stream` waterfall), so main-loop turns, sub-agents, compaction and title calls are all recorded — consistent with the platform bill; - The session badge on the stats line is an **estimate at the current tier**; today/month/total and budget are **exact billing at the actual call time**; - Budget warnings are **advisory only** — they never block calls; - The price table **auto-syncs the latest official prices at every startup** (falls back to existing prices with a warning on failure); set the `DSH_BILLING_SKIP_STARTUP_SYNC=1` env var to disable it (offline/test environments); manual price editing is no longer available. ## Data storage - Ledger: `$DSH_HOME/storages/dsh-billing/ledger.json` (atomic writes + 2s debounce; pruned by `historyDays`, max 200 session entries per day); - All settings changes auto-save immediately (submitted via the settings page "Save", validated and persisted server-side); - Delete the ledger file or use "Clear all history" in the settings page to reset. ## Architecture ``` dsh-billing ├── cordis.patch.yml # bundle patch: inserts the billing row into the web profile ├── package.json # dsh.bundle patch declaration + dsh.client browser declaration ├── lib/ │ ├── index.js # host plugin: llm/stream billing wrap, costUsage session projection, │ │ # billing service (hand-written typertRemote binding), balance query, official price sync │ ├── pricing.js # official price table, official-page HTML parser, peak/off-peak math │ ├── store.js # ledger persistence & config management ($DSH_HOME/storages/dsh-billing) │ ├── typert.host.js # ./typert export: Typert manifest (auto-registered by typert-loader) │ └── client.js # ./client export: browser single-file bundle (stats line / sidebar / settings) └── test/ ├── verify.mjs # unit verification: pricing/config/ledger/projection/real cordis load/billing wrap └── demo.mjs # billing demo: simulated multi-session/multi-model peak & off-peak calls → ledger & summary ``` Data channels: - **Session cost**: the host registers the `costUsage` session projection (pure token buckets split by model); the browser reads it via `useProjection('costUsage')` and prices it at the current tier; - **Ledger / budget / balance / config**: `billing/getState | updateConfig | refreshBalance | fetchPrices | resetHistory` over the Typert gateway RPC (`remote.billing.*`); - **Balance**: official `GET {baseURL}/user/balance`, reusing the same API key as model requests (credentials service / env), cached in-process by `refreshMinutes`; **the "Refresh balance" button forces a fresh query on every click** (a sequence guard keeps only the latest result); - **Refresh strategy**: the host deliberately avoids the cordis timer mixin (`ctx.interval` throws on contexts without a mounted timer service) — everything is "cache timestamp + on-demand refresh" driven by client polling/manual refresh. The plugin imports no cordis/dsh Service/Context runtime classes (only Node built-ins, zod, and pure functions from dsh-home-paths / dsh-credentials), sharing the host's single runtime instance with no duplicate-dependency risk. ## Installation > Requirements: Node.js ≥ 20 + DeepSeek Harness (a build with the `dsh plugin` command, `npm install -g @deepseek-ai/dsh`). ### From GitHub (after publishing) ```sh dsh plugin --profile web add github:mahiro6/dsh-billing ``` Or via a tarball URL: ```sh dsh plugin --profile web add https://github.com//dsh-billing/archive/refs/heads/main.tar.gz ``` ### Local development install ```sh dsh plugin --profile web add link: # e.g. dsh plugin --profile web add link:E:\Workspace\dsh-billing ``` ### Activation & removal Restart **`dsh web`** after installing (plugin rows, Typert manifests and the client bundle are all scanned at startup): ```sh dsh web ``` Update: re-run the install command and restart. Remove: `dsh plugin --profile web remove dsh-billing`. ## Development & verification ```sh pnpm install # dependencies node --check lib/index.js && node --check lib/pricing.js \ && node --check lib/store.js && node --check lib/typert.host.js \ && node --check lib/client.js # syntax checks node test/verify.mjs # unit verification node test/demo.mjs # billing demo dsh --profile web --dump-config # composed-tree check (after install) ``` ## Configuration > These fields persist in `ledger.json`. **None of them except `budget` is editable in the UI anymore**: display-related fields stay at their defaults, and the price table plus peak/off-peak times are maintained by the official sync. You can still adjust them by editing the ledger file. | Field | Default | Notes | | --- | --- | --- | | `locale` | `auto` | UI language: auto (browser) / zh / en (fixed default) | | `statsLine` | `true` | Show balance, today's cost & session cost on the stats line (fixed default) | | `sidebar` | `true` | Show balance, today's cost & budget bar in the sidebar footer (fixed default) | | `currency` | `CNY` | Display currency (CNY/USD/EUR/GBP/JPY/HKD/TWD, fixed default) | | `symbol` | `¥` | Currency symbol (fixed default) | | `decimals` | `4` | Money decimals (fixed default) | | `exchangeRate` | `7.2` | Exchange rate (1 USD = ?, fixed default) | | `peakEnabled` | `true` | Enable peak/off-peak pricing (fixed default) | | `peakEffectiveAt` | `2026-08-16T16:00:00Z` | Peak pricing effective time (UTC, maintained by the official sync) | | `peakWindows` | `[{1,4},{6,10}]` | Peak hour windows (UTC hours, maintained by the official sync) | | `prices` | bundled DeepSeek official | Per model `cacheHit/cacheMiss/output` + optional `offPeak/peak` (USD / 1M tokens, maintained by the official sync) | | `budget` | `{enabled:false, amount:100, period:'month'}` | Budget: enabled / amount (display currency) / period (day/month/all/custom) / custom range (**editable in the UI**) | | `balance` | `{display:'both', refreshMinutes:5}` | Balance display locations & auto-refresh interval (fixed default) | | `historyDays` | `180` | Ledger retention days (7–3650, fixed default) | ## Known limitations - Balance is account-level data, cached per `refreshMinutes`; manual refresh is immediate; - The session badge on the stats line is an estimate at the current tier; exact figures come from the ledger (today/month/total); - Official-page parsing depends on the current page structure; if the page is redesigned, "Sync official prices" will fail and the price table keeps the previous values — retry later or edit `ledger.json` as a fallback; - Price sync **replaces the whole model set with the official page's models**: legacy/custom models absent from the official page are removed; - Balance lookups need network access to api.deepseek.com and a valid API key; **the API key is only ever sent to the official domain** (requests are refused when baseURL points elsewhere; model calls are unaffected); - Installing/updating requires a `dsh web` restart. ## Contributing Issues and PRs are welcome. Please make sure `node test/verify.mjs` passes before submitting. ## License [MIT](LICENSE) © 2026 dsh-billing contributors