VviLliAm-qwq/dsh-peak-balance

dsh-peak-balance

Peak/off-peak billing clock with live DeepSeek balance and per-turn cost above the dsh-tui prompt, plus an optional flashing peak-hour warning frame.

Uncategorizedbalancecost-trackingdeepseek

dsh-peak-balance

English · 中文

Peak/off-peak billing clock, live DeepSeek account balance, per-turn cost and a /hist token-history grid — inside dsh-TUI.

⚡ 峰时 09:00-12:00 · 距谷时 1h23m · 本轮 ¥0.0234 · 余额 ¥42.10

(The order is phase → countdown → turn → balance: a narrow terminal truncates the tail, and the per-turn cost is the figure that changes while you watch. While the model is still answering, the turn figure is live — rendered as 本轮·计费中 ¥… and refreshed on every usage report — and it freezes to 本轮 ¥… once the turn closes.)

While the peak window is active you can turn the line into a three-row frame that pulses in one of seven colors:

╭──────────────────────────────────────────────────────────╮
│ ⚡ 峰时 · 距谷时 1h23m · 本轮 ¥0.0234 · 余额 ¥42.10   ▂▃▄▅▆▇ │
╰──────────────────────────────────────────────────────────╯

/hist (also /tokenhistory, alt+h, or /th with a trailing space — see below) takes over the whole terminal with a GitHub-style contribution grid:

╭─ 🐋 Token history  Total tokens  26w  subagents counted ───────────────── ✕ ─╮
│ updated 12:04:11 · 341 sessions · 6,706 reports · 9 active days               │
│ ───────────────────────────────────────────────────────────────────────────── │
│      6月      7月      8月      9月                                           │
│ Mon  ▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢                                               │
│ Wed  ▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢                                               │
│ Fri  ▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢                                               │
│                       ▲                                                       │
│ Total tokens less ▢▢▢▢▢ more · peak 251,442,584                               │
  ╭─ 2026-09-11 Fri ───────────────────────────────────────────────────────╮
  │ tokens      input(miss) 2,065,340 · cache read 224.3M · output 1.5M       │
  │ cost        ¥18.8494                                                      │
  │ cache hit   99.1% · subagent share 12.4%                                  │
  │ model       deepseek-flash 227.9M · deepseek-v4-pro 3.2M                  │
  ╰───────────────────────────────────────────────────────────────────────────╯
│ Total       1,079,834,040 · Est. cost ¥64.6867 · cache hit 99.0%              │
│ subagents   36/341 sessions · 945 reports · busiest 2026-09-11                │
│ ───────────────────────────────────────────────────────────────────────────── │
│ model                              tokens   cost(est) hit rate    rates       │
│ ───────────────────────────────────────────────────────────────────────────── │
│ deepseek-flash                     587.3M      ¥33.94    99.3% built-in       │
│ deepseek-v4-flash-vision-exp       412.6M      ¥24.53    98.8% built-in       │
│ deepseek-v4.1-flash-expires…        68.2M           —    98.5%  unknown       │
│ deepseek-v4-pro                      8.0M       ¥4.28    95.4% built-in       │
│ deepseek-v4-flash                  457.2k     ¥0.0975    90.1% built-in       │
│ ←/→ week · ↑/↓ day · t today · m metric · w span · s subagents · r rescan · q/Esc close │
╰───────────────────────────────────────────────────────────────────────────────╯

Features

FeatureWhat it shows
Peak / off-peak clockThe billing window currently in force and a live countdown to the next price switch (09:00-12:00 / 14:00-18:00 Beijing time, Monday–Friday; weekends are off-peak all day).
Account readout (provider-neutral)The balance or plan quota of the provider the focused conversation actually runs through: an official balance, a subscription's 5-hour/weekly/monthly windows, a relay's credit pool. The provider comes from request/header.config.provider in the session log, and its base URL and credential reference are resolved through the harness seams. A conversation restored at boot names no route until its first request (the host replays its history without re-emitting events), so auto follows the host's persisted /model route — the one the next request will use — until the conversation speaks for itself. A provider with no readable interface is simply left off the line — no figure is ever guessed.
Per-turn costWhat the turn that just finished cost. When the provider exposes a spend counter (Command Code credits, OpenRouter key usage, a relay's used quota) this is measured from that counter; otherwise it is estimated from a rate card, and otherwise the line says the model is unrated. The figure follows the conversation you are focused on, not the one that last appended an event.
Quota diagnostics /quotaOne command shows what is being read, which adapter answers, why the last read failed, and every provider route this process could ask about (/quota check <provider> probes one on the spot).
Peak-hour warningOptional. While peak pricing is active the status contribution becomes a rounded frame whose border, phase label and travelling waveform pulse in the chosen color.
History grid /thA full-screen scene: one square per day, shaded by that day's usage, with a hover card for the day under the pointer. Keyboard: ←/→ walks days, ↑/↓ walks weeks, m cycles the metric, w the span, s the subagent switch, r rescans, q/Esc returns to the conversation.
Totals and per-model statsTotals: tokens, estimated cost, cache-hit rate, active days, sessions, subagent share, busiest day. Model table: each model's total tokens, estimated cost, cache-hit rate and where its rates came from. The totals row covers all history (it does not follow the grid's span) and says so inline.
Custom rates /th pricePrice a model the embedded card does not list; until you do, it reports tokens with an explicit "unrated" marker instead of a guessed amount.
Follows the UI languageInstant hand-off with dsh-TUI's /lang: the status line, the history scene and every command reply switch with it. The host mirrors the choice into its dsh-tui settings namespace and the plugin listens for settings/updated; a 1 s poll of ~/.dsh-tui/lang.json covers hosts that serve no such namespace. The settings card and the command-completion descriptions were already bilingual.
Settings subpageThe Peak & Balance card gains a Token history subpage with ten options.

Install

# from npm
dsh plugin --profile dsh-tui add dsh-peak-balance

# ...or straight from a checkout of this repository (pnpm packs the local
# directory, so the profile keeps a real copy instead of a symlink)
dsh plugin --profile dsh-tui add file:/absolute/path/to/dsh-peak-balance

The command appends the bundle row to the profile's dsh.profile.bundles. Then restart the TUI (/restart inside dsh-tui) so the profile loads the new row; the settings card appears under /settings immediately after the restart.

Installing by symlink (link: or a directory junction) is not recommended: Node resolves a plugin's real path, and @deepseek-ai/* must stay reachable from it. file: and npm installs both leave a real directory inside the profile, which is the layout the host expects.

Updating a file: install: pnpm caches a local directory dependency, so add/update alone will not pick up edited sources. Remove and re-add it (after a version bump) to refresh the profile copy:

dsh plugin --profile dsh-tui remove dsh-peak-balance
dsh plugin --profile dsh-tui add file:/absolute/path/to/dsh-peak-balance

Settings

/settings → the Peak & Balance card. Edits are written live; no restart is needed.

Main card:

FieldTypeDefaultMeaning
Show balancebooleantrueShow the account section on the status line (the provider's quota meter, as an amount or a percentage).
Show per-turn costbooleantrueShow what this turn has cost: a live estimate while the model answers, frozen when the turn closes (measured from the provider's counter when it has one).
Peak-hour warningbooleanfalseTurn the line into a pulsing frame while peak pricing is active.
Warning colorselectredFrame color: red orange yellow green cyan blue purple.

Provider quota subpage:

FieldTypeDefaultMeaning
Quota providertextautoauto follows the focused conversation's provider, falling back to the host's persisted /model route while a restored conversation has not sent its first request; a provider id (commandcode, openrouter, …) pins that one; off hides the account section.
Quota metricselectautoauto (the tightest window), balance, window5h, windowWeekly, windowDaily, windowMonthly, planRemaining, keyLimit, periodSpend, lifetimeSpend, or rotate (cycle every meter, one every 8 s).
Turn spendselectautoauto (measured when the provider has a counter), measured, estimate.
Unofficial endpointsbooleanfalseAllow quota reads from endpoints no provider documents publicly (mostly reverse-engineered console APIs).
Provider spec filetextemptyJSON file describing providers this build ships no adapter for; empty uses ~/.dsh-tui/dsh-peak-balance-providers.json.
Billing modeselectautoHow the account readout is shaped: auto follows the provider's declaration (a subscription shows percentages, pay-as-you-go shows amounts), or force money / plan.
Percentage baseselectmeterWhat a plan percentage divides by: meter uses the window the line is showing, monthly the whole monthly pool.

Token history subpage:

FieldTypeDefaultMeaning
Grid metricselecttokensWhat the squares shade: tokens, cost, output, cacheMiss.
Time spanselect26Week columns drawn: 13 / 26 / 53.
Count subagentsbooleantrueSubagent sessions spend real tokens; off reports only your own conversations.
Week starts onselectmonWhich weekday is the grid's first row (mon / sun).
Grid paletteselectgithubgithub (green), blue, or theme (the active accent).
Scene layoutselectcardcard frames the scene in a rounded box with section separators; plain drops the chrome (2 rows / 4 columns cheaper). Card falls back to plain by itself on a small terminal.
Hover: tokensbooleantrueShow the input / cache-read / output split in the day card.
Hover: costbooleantrueShow the hovered day's estimated cost.
Hover: cache hit ratebooleantrueShow the hovered day's cache-hit rate.
Hover: modelsbooleantrueShow which models the hovered day used.

The settings live in the dsh-peak-balance namespace of the dsh settings document, so they can also be edited there directly. The scene's m / w / s keys are settings changes: they write back immediately (and survive a restart); if the write is refused the change stays for the current session and a warning is logged.

/hist — token history

/hist                                 # open the grid (recommended: no built-in collision)
/tokenhistory                         # the long alias
alt+h                                 # same thing without typing
/th                                   # works too, but keep the trailing space (see below)
/hist price                           # list custom rates and unrated models
/hist price set <model> <hit> <miss> <out> [peakHit peakMiss peakOut]
/hist price rm <model>
/hist price clear
/quota                                # quota diagnostics: source, adapter, meter, last failure
/quota check <provider>               # probe one provider now (empty = the current target)
/quota meter windowWeekly             # switch the status-line meter (same as the setting)

Why a bare /th + Enter switches the theme

A host behaviour the plugin cannot change, stated plainly: while dsh-tui's slash-completion overlay is open, Enter runs the highlighted suggestion, not the line you typed (handleEnter in PromptInput.js); the merged list puts built-ins first and appends plugin commands, and the selection resets to the first row. Typing /th matches theme, thinking and our th, so the highlight sits on theme and Enter switches the theme.

Entry pointNote
/histRecommended. hist is not a prefix of any built-in, so the menu offers only it and Enter runs it.
/tokenhistorySame, no collision.
alt+hOpens the scene straight from the chat state.
/th + EnterKeep the trailing space: the menu closes (no children), and Enter dispatches th.

A future SKILL whose name starts with hist would capture /hist's Enter the same way — same host logic; rename then (it is one constant in the code).

Keyboard

KeyEffect
← / →Move to the square to the left/right (same weekday, one week)
↑ / ↓Move to the square above/below (same column, one day)
tJump back to today
m / w / sCycle metric / span / subagents (the matching chip flashes; the change is persisted)
rRescan
q / EscBack to the conversation

Moves stop at the grid edge and never enter a day that has not happened yet (no wrapping). The selected square is lightened and a ▲ under the grid points at its column.

Subagent feedback: the title bar carries a permanent chip (subagent counted — green — or subagent excluded), pressing s inverts it for 1.2 s, and the totals row spells out excluded N sessions / M reports instead of just shrinking the numbers.

The data source is this machine's session logs ($DSH_HOME/sessions/). Every assistant/message event in those logs carries the usage DeepSeek returned for that request (inputTokens / cacheReadTokens / outputTokens / cacheWriteTokens); the plugin files each report into the Beijing calendar day and the peak/off-peak tier of its own timestamp, then per model. So:

  • tokens are the provider's own reported numbers, not a local estimate;
  • money is an estimate (the API returns tokens, never money), converted with the embedded rate card or your custom rates, and labelled as such;
  • only this machine's dsh usage is covered — web chat or other clients are not;
  • the covered range is whatever this machine's logs still hold.

Subagents count by default (they spend real money), stay distinguishable, and can be switched off.

Unrated models (not on the embedded card) show tokens with — for money until you give them rates through /hist price set. Custom rates are written immediately to ~/.dsh-tui/dsh-peak-balance-rates.json and feed both the history view and the status line's per-turn figure. <hit> <miss> <out> are the off-peak prices in CNY per million tokens; peak defaults to twice those (the official rule), and you can pass three more numbers to set the peak tier explicitly.

Two implementation details that decide the accuracy (both reproducible on real logs):

  1. The logs are multi-frame zstd (one frame per append). zlib.zstdDecompressSync stops after the first frame, and scanning for the magic bytes can hit the magic inside a compressed block — where a truncated decode still "succeeds" with partial content and silently drops events. The plugin parses the zstd frame and block headers to compute each frame's exact length: on this machine's 341 logs the byte-scanning approach lost 282 events, the header walk recovers all of them.
  2. Fork/rewind logs physically carry their parent's event prefix. The cut is the header's seedLength (the first session/end-seed event sits on it); without it a naive sum counts the parent's usage twice — 292 usage reports (~4.5%) across this machine's nine seeded logs.

Cache. Activation reads the incremental cache and paints the grid from it, so opening the scene is instant; the scan that follows only refreshes what changed. The cache is incremental three ways: a log whose (path, size, mtime) is unchanged is skipped outright; a log that was only appended to is finished from the frame the cache stopped at (a done offset, guarded by a 16-byte signature of the bytes just before it), so only the new frames are decompressed; and a cache written by an older version is migrated in place rather than thrown away. Measured on this machine (437 logs, 112 MB, 190k frames): after the largest log grows, a rescan fell from 314 ms to 17 ms, and the first scan after a plugin update fell from a full rebuild to 81 ms. An up-to-date corpus lands in 20–30 ms. The cache lives at ~/.dsh-tui/dsh-peak-balance-history.json (safe to delete — it rebuilds). The one genuinely slow case is that first rebuild on a large corpus (≈ 5–6 s here, of which ≈ 4.3 s is decompressing 190k small zstd frames one at a time — a Node-side floor); it checkpoints as it goes so an interrupted run resumes instead of starting over, and it paints as it goes, newest logs first, so the recent weeks are on screen within the first second. While the scene is open it re-scans once a minute behind the figures (the status line shows refreshing); closing it stops that.

How the numbers are produced

Peak window. Off-peak pricing is half of peak pricing; peak hours are Beijing time (UTC+8) Monday–Friday 09:00-12:00 and 14:00-18:00, everything else — including both weekend days — is off-peak.

Cost. DeepSeek's API returns token counts, never money, so the per-turn figure is an estimate:

cost = inputMissTokens   × inputMissRate
     + cacheReadTokens   × inputHitRate
     + outputTokens      × outputRate

The three input-side figures are disjoint: inputTokens is the prompt that missed the cache, cacheReadTokens is the prompt served from cache, and the provider's own totalTokens is their sum plus the output. Treating the cache read as a subset of the input (clamping one against the other) under-prices a cached turn by several times — measured against a real balance drop on 2026-09-10, a turn that cost ¥0.20 was estimated at ¥0.03 that way and ¥0.23 with the formula above.

Each provider usage report is filed into the peak or off-peak bucket using the timestamp of the request that produced it, so a turn straddling a price boundary is not priced wholesale at the current window.

Embedded rate card (CNY per million tokens), verified 2026-09-10 against the official Models & Pricing page:

ModelBucketInput (cache hit)Input (cache miss)Output
deepseek-flashoff-peak0.0214
deepseek-flashpeak0.0428
deepseek-v4-prooff-peak0.154.513.5
deepseek-v4-propeak0.30927

The legacy ids deepseek-v4-flash and deepseek-v4-flash-vision-exp are served by DeepSeek-V4.1-Flash and priced at the Flash rates; deepseek-v4-pro is rerated to the Flash card from 2026-09-14 12:00 Beijing, when that route is retired to V4.1-Flash. A model the card does not know is reported as 费率未知 / unrated model — the plugin shows tokens instead of a wrong number.

Balance. GET https://api.deepseek.com/user/balance (the same read-only endpoint behind dsh-tui's /balance command). The key is resolved through the credentials seam (DEEPSEEK_API_KEY) with an environment fallback, is sent only in the request header, and is never logged or stored by this plugin.

Third-party providers

Since 0.4.0 the account section is no longer DeepSeek-specific. The plugin resolves, in order, which provider to ask, where it lives and how to ask it — and degrades quietly at every step it cannot complete:

  1. Provider — the focused conversation's last request/header.config.provider (in auto mode; the setting can pin one provider or turn the section off). A conversation restored at boot has published no header yet, so auto then uses the host's persisted /model route (~/.dsh-tui/model.json, the file the picker re-applies after a restart); the built-in deepseek-official default is the last resort, for a host that persists no choice at all.
  2. Endpoint and credential — the provider's own settings section (pointed at by ctx.llm.listConfigurableProviders()) → the spec file → the generated catalog snapshot. The key is resolved through the credentials seam with an environment fallback of the same name.
  3. Adapter — by provider id, then by base-URL host, then by spec. Nothing matches → the section is left off the line (it only says no API when you pinned that provider yourself).

Built-in adapters

AdapterProvidersMetersVerified against a real account
deepseek-balancedeepseek-official / deepseekbalance, in the reported currency✅ yes
commandcode-plancommandcode5-hour and weekly windows, monthly plan remainder, period spend✅ yes (a live GOAT plan)
openrouteropenrouterbalance (credits − usage), key limit, daily/weekly/monthly usage⚠️ no — built from the official docs, tested with fixture payloads
moonshot-balancemoonshotai / moonshotai-cnbalance (CNY on .cn, USD internationally)⚠️ no
siliconflow-balancesiliconflow routesbalance (the API states no currency, so none is printed)⚠️ no
openai-billinggateways declared in the composition (One API, New API, self-hosted)soft/hard_limit_usd (one grant, both fields equal) minus total_usage (cents); the unit follows the gateway's own display setting⚠️ no
declaredanything in the spec filewhatever the spec says—

OpenRouter's /credits requires a management key (an ordinary key is refused with 403), so the adapter asks /key in parallel and still reports the key's limit and usage when only an ordinary key exists.

Plans read as percentages

The shape of the account readout follows the provider's billing method: a pay-as-you-go account (a currency balance, debited per token) shows amounts, while a subscription (capped rolling windows over a monthly pool) shows percentages — how much of the cap is left, and what share of it the turn just drew. The order is: the Billing mode override, then the provider's own declaration (an adapter, or a spec stanza's billing), then the data (capped windows with no currency balance read as a plan). The inference is deliberately conservative: an unrecognized shape reads as money — absolute figures — rather than inventing a denominator.

Percentage base picks the denominator: the window the status line is showing by default (the tightest limit — the one whose reset countdown is right there), or the whole monthly pool. Each falls back to the other, so a provider reporting only one of them still gets a usable divisor. No cap, no percentage: the figure falls back to absolute amounts.

🌊 off-peak · weekend · peak in 18h26m · turn 1.79%(0.2500) · plan GOAT · 5h 92.9% left(1.00/14.00) · resets in 1h00m

A narrow terminal keeps the percentage and drops the rest. Every part may carry a compact form (turn 1.79%); when the full line does not fit, the compact forms are swapped in from the right, which is where the host truncates first. The width comes from the host's terminal-size hook when there is one, then process.stdout.columns; with neither, the line renders in full exactly as before.