dsh-gauge
中文 | English
Accurate cache-hit rate, token usage, and cost estimates for the DeepSeek Harness Web UI.
The official stats line rounds the cache-hit rate to an integer — Math.round turns 99.8% into a misleading 100%. dsh-gauge replaces it with a precise one-decimal (configurable) figure, adds a per-bucket token breakdown, a session usage panel, and a session cost estimate that automatically follows the official DeepSeek price changes (using the official API).
Features
- Precise cache-hit rate —
cacheRead / (cacheRead + uncached + cacheWrite)with a configurable
number of decimals (default 1), so 99.8% stays 99.8%.
- Bucket breakdown — cache-hit / cache-miss input tokens and output tokens. The write bucket is
automatically hidden when it is 0 (the opencode-go/pi-ai adapter never reports cache-write tokens, so it is always 0 in practice).
- Cost estimate + price-change comparison — estimated from actual usage × model prices
(deepseek-v4-flash / deepseek-v4-pro, auto-detected from the conversation). Before the new official prices take effect it shows current cost + upcoming price + projected increase; at the switch moment it automatically moves to the new prices.
- Per-request peak/off-peak billing — the estimate is not a "current time" snapshot: every
assistant message is priced by its own timestamp, so tokens consumed during peak hours are billed at the peak rate and off-peak tokens at the discounted rate, then summed.
- Peak-hour badge — Beijing 09:00–12:00 / 14:00–18:00 are DeepSeek's peak-pricing windows. The
stats-row tail shows peak/off-peak according to the current Beijing hour (also after the price switch).
- Usage panel — a ⓘ trigger in the session header opens the session's model, hit rate, billed
input, per-bucket totals (full numbers by default), context occupancy, and cost estimate (with the post-switch comparison).
- Dual-row stats (official metrics + precise usage) — the first row keeps the official session
metrics (turns/steps, LLM/tool-call time, TTFT avg, tok/s) in a replica of the official styling (line height deliberately tightened so the two rows read as one block); the second row is the plugin's precise usage (hit rate, buckets, output, cost, peak/off-peak badge). With replaceNativeStatsLine: false the official native row (including its usage segment) stays.
- Bilingual & currency-aware — when the UI is English, all text switches to English and the cost
is estimated in USD with the international price table. Locale and currency follow live changes, no restart needed.
Screenshots
Install
# One command (recommended): dsh plugin runs pnpm add and automatically
# appends any dependency that declares dsh.bundle to dsh.profile.bundles
dsh plugin --profile web add dsh-gauge
# Manual: cd ~/.dsh/profiles/web && npm install dsh-gauge, then edit
# profiles/web/package.json and add "dsh-gauge" to dsh.profile.bundles
# restart
dsh webYou should see the precise stats row under the composer and the ⓘ usage trigger in the session header.
> Local development: install from source instead — pnpm add file:C:/Object/dsh-plugin/dsh-gauge > (run npm run build after source changes; useful when tuning src/config.ts prices/peak windows).
Works out of the box & the config card
After install and restart everything works with zero configuration:
- Two stats rows under the composer: the precise cache-hit rate (99.8% stays 99.8%), bucket
breakdown, output tokens, estimated cost, and a peak/off-peak badge;
- The ⓘ usage panel in the session header: full token totals, context occupancy, model, cost and
the post-switch price comparison;
- UI copy and the cost currency follow the interface language automatically.
The dsh-gauge card under Settings → Plugins → configurable: current DSH versions hard-code the plugin settings exposed to web clients in an allowlist (dsh-host-apiproxy's WEB_SETTINGS_NAMESPACES), so a third-party plugin needs a one-time gauge allowlist entry for the card to show up (steps in Troubleshooting). Not adding it affects nothing else — alternatively edit cordis.patch.yml directly (see Configuration).
> Local development: replace step ② with pnpm add file:C:/Object/dsh-plugin/dsh-gauge — after > source changes run npm run build and restart; useful when tuning src/config.ts prices/peak > windows.
How it works
- The metrics row registers into the
conversation.composer.dockslot atpriority: -1in the
official stats cell, shadowing the native row; the usage row is added at order: 1. With replaceNativeStatsLine: true (default) the native usage segment is replaced by the precise row; with false the native row (including its usage segment) stays.
- Token totals come from the
tokenUsageprojection (@deepseek-ai/dsh-token-meter); context
occupancy from contextPressure.
- The cost estimate pages the full session history (
sessions.history): each finalized assistant
message carries its own completion time and usage, so every request is priced with the rate applicable at that moment (peak or off-peak for the new scheme; flat for the current pre-change prices) and the results are summed — history before "load earlier" is priced precisely too, so a session with 200M hit tokens never shows a few cents of cost.
- The active model is derived from the last assistant message of the full history (
source.model;
without a connection surface it falls back to the trajectory view's requestConfig.model), or fixed via the model config key.
- After compaction, old events are replaced by summaries: the estimate and the official
tokenUsage projection are based on the same event set, so they stay consistent (the official projection discards compacted usage the same way).
Configuration
Most users need zero configuration — the plugin works out of the box. The keys below can be tweaked through Settings → Plugins → dsh-gauge — saved changes apply immediately, no restart needed (the one exception is replaceNativeStatsLine, which decides the shadow registration and needs a restart), or by patching the gauge row in ~/.dsh/profiles/web/cordis.patch.yml:
| Key | Default | Meaning |
|---|---|---|
showPrice | true | Show the cost estimate (stats line + panel) |
showPeakBadge | true | Show the Beijing peak-hour badge in the stats line |
replaceNativeStatsLine | true | Replace the official stats row (false keeps the official native row) |
hitRateDecimals | 1 | Decimals for the cache-hit percent (0–2) |
tokenDecimals | 1 | Decimals for the compact K/M token format (0–2) |
panelExactTokens | true | Show full token totals in the panel (false uses K/M abbreviations) |
currency | auto | auto follows the UI language (English → $ + USD plans, otherwise ¥ + CNY plans); or set ¥ / $ explicitly |
model | auto | auto derives the model from the session; or set a model id explicitly |
Developer-only (not exposed in the UI) — the peak windows peakHours, the CNY price table pricePlans, the USD price table usdPricePlans, the effective-date nextFrom, and the off-peak factor offPeakFactor are built into src/config.ts with DeepSeek-official defaults. Change them by editing that file; ordinary users should not override them.
# ~/.dsh/profiles/web/cordis.patch.yml — flat loader patch entries
- id: gauge
config:
showPrice: true
showPeakBadge: true
hitRateDecimals: 1
tokenDecimals: 1
panelExactTokens: true
currency: autoBuilt-in prices & the price switch — the current flat prices (before the switch) and the new peak/off-peak official prices are baked into src/config.ts (pricePlans for the CNY table; usdPricePlans for the international USD table). The estimate walks each assistant message and prices it by its own timestamp (peak rate during peak windows, half price otherwise), and automatically switches to the new prices at nextFrom. There is no separate "cache write" bucket in the official price list.
> The cost is an estimate; the official bill is authoritative. Patch entries are flat > {id, ...} loader entries — there is no update:/disable: wrapper; writing - update: is > rejected with an error. If the config card is missing, see the whitelist note in Troubleshooting.
Comparison with dsh-usage
dsh-usage (v0.1.0) appeared the same day this plugin was written, so here is an honest, source-level comparison.
| Dimension | dsh-usage | dsh-gauge |
|---|---|---|
| Cache-hit rate (%) | — no hit-rate figure at all, only raw cache tokens | precise hit rate with configurable decimals (99.8% stays 99.8%) |
| Peak/off-peak pricing | — no peak/off-peak handling; bundled rates are the 2026-04-24 USD table, so the estimate goes stale after the 2026-08-16 price change | per-message-timestamp peak/off-peak pricing, current-vs-next price comparison with automatic switch at the effective date |
| Granularity | per-turn readout under each assistant message + Settings Usage page (52-week heatmap, provider/model totals, cross-session) | session-scoped stats row (replaces the native row) + header ⓘ panel (model, buckets, context occupancy, cost) |
| Cost accounting | replay-derived modelCost projection, effective-dated rates, coverage notes for unpriced/without-usage calls | tokenUsage projection × built-in price tables (CNY + USD) |
| Data source | durable log replay (survives paging/compaction) | tokenUsage/contextPressure projections |
| Language | English-only UI text | bilingual (zh/en) |
| Native row | adds its own row | shadows the official row by default |
| Cache-write bucket | priced as its own bucket | hidden while 0 |
In short: dsh-gauge is a precision/efficiency gauge — the exact cache-hit rate the official UI rounds away, peak-hour awareness, and a live cost check that follows the new peak/off-peak pricing without a maintenance burden. The two are complementary and can be installed side by side — different slots, different ids, no conflict.
Troubleshooting
- "写入" (write) is always 0 — by design: some adapters never report cache-write tokens, so the
bucket is hidden while zero. If a future provider reports writes, it reappears.
- Price/cost looks wrong — the estimates use the built-in price tables in
src/config.ts; adjust
pricePlans/usdPricePlans there, or set currency/model explicitly. The cost is an estimate; the official bill is authoritative.
- Changes don't show up — except
replaceNativeStatsLine(which decides the shadow
registration), saved config changes apply immediately; if you changed cordis.patch.yml instead, restart dsh web.
- No dsh-gauge card under Settings → Plugins → configurable — current DSH versions hard-code
the namespaces exposed to web clients in dsh-host-apiproxy's WEB_SETTINGS_NAMESPACES allowlist (the official comment marks plugin self-registration as deferred work); a namespace absent from the allowlist is never returned by settings.describe even when its owner registered it, so the card stays hidden. Add gauge to the allowlist in the host installation and restart dsh web:
``js // <dsh install>/node_modules/@deepseek-ai/dsh-host-apiproxy/lib/index.js const WEB_SETTINGS_NAMESPACES = [ "agent-loop", "shell", "locale", "permission", "ui-conversation", "ui-theme", "web-search-deepseek", "gauge", // ← add this line ]; ``
Without the allowlist entry the stats row and usage panel still work — only the config card is hidden (you can still configure via cordis.patch.yml). The requirement disappears once DSH opens plugin self-registration.
- The page fails to boot — make sure
lib/client.jsis the bundled client artifact (run
npm run build, which produces the __ModuleLoader__.load format; a raw tsc ESM output breaks the page).
Development
pnpm install
pnpm run typecheck
pnpm test
pnpm run buildnpm run build compiles with tsc and then bundles the client entry with scripts/build-client.mjs into the DSH client-module loader format.
License
MIT