bowenliang123/dsh-context
bowenliang123/dsh-contextDSH 上下文洞察面板:Context 仪表盘 + /context命令 + Context 浏览器,查看 Context的分类组成、内容详情、演进趋势、压缩/注入事件、统计等一站式 Context 全生命周期管理。
可审计的 DSH token 账本:把持久化会话日志折算成重启安全的总量,可从原始日志重算并比对差异,并按工作区、会话、模型或厂商生成账单,可导出 CSV 或 JSON。
Transparent, auditable token accounting for DeepSeek Harness.
A ledger, not a dashboard. It folds the durable DSH session log into token totals, keeps them restart-safe, and — the part that matters — lets you recompute the exact same numbers from the raw logs and diff them against what the running host recorded.
$ dsh-token-ledger audit
scanned 139 session log(s), 344203 events, 29 forked session(s), 0 unreadable
stored 6901 calls 1205685663 tokens
recomputed 6901 calls 1205685663 tokens
audit: match — the stored ledger equals a fresh fold of the raw logs
If you only want a pretty chart, there are a dozen plugins for that. This one is for when you need to defend the number.
Since 0.5 the settings page has three tabs: Overview (the ledger, with what each period cost), Rates (a price list for each vendor's newest models, with the live USD/CNY rate) and Bill (the same usage turned into money — four groupings stacked over five periods each, exportable as CSV or JSON in one file). The bill names the price source, the rate and every price it could not apply, so the number can be checked. See Settings page.
The settings page's tabs, captured on a real ledger — the totals are this machine's:
Overview, with the month's totals, today's usage and a year of activity:
The calendar, which is where a month's shape lives — busiest day, lightest day, the day that ran latest — above the same usage broken down by model:
The last seven days as bars, with the same by-model breakdown:
Rates: the live USD/CNY rate and each vendor's published price list, each price editable by hand for the times the network is not there:
| Property | Why it matters |
|---|---|
| Nothing is installed for you | No runtime dependencies, and every declared peer is an optional host package — so a version drift in a DSH package cannot pull a second copy of the harness into your profile. |
| Zero install scripts | dsh plugin add from a git URL works immediately — pnpm has no build to block and no allowBuilds entry to add. |
Only node: imports | The host half loads from any profile (web, desktop, headless, TUI) without resolving a single package. |
| No model-facing surface | It registers no prompt section, no message and no tool, so it cannot change a request prefix or hurt KV-cache reuse. |
| Fails soft | Every hook is guarded. A ledger fault logs a warning; it never fails a session. |
# from npm
dsh plugin --profile web add @chenmiao8563/dsh-token-ledger
# from a git URL (no build step is involved)
dsh plugin --profile web add github:chenmiao8563/dsh-token-ledger
# from a local checkout
dsh plugin --profile web add /absolute/path/to/dsh-token-ledger
The npm package is scoped because npm rejects the unscoped name
dsh-token-ledger as too similar to an existing package once separators are
normalized away. The CLI command is still dsh-token-ledger.
Restart DSH, then check the row landed:
dsh --profile web --dump-config | grep token-ledger
In the conversation, /tokens prints the ledger:
/tokens
/tokens export # write CSV + JSON under <DSH_HOME>/token-ledger/exports/
/tokens json # the raw snapshot
/tokens path # where the ledger file is
The ledger is written to <DSH_HOME>/token-ledger/ledger.json.
DSH ^0.2.0-rc.1 — 0.2.0-rc.2 is the version this is currently verified against, and
the floor is written as 0.2.0-rc.1 because that is how this ecosystem spells a
prerelease range. The ^ puts the ceiling at 0.3.0.
This range is load-bearing rather than decorative: DSH compares it against the running
harness at startup and skips a plugin whose range excludes it. ^0.1.2-rc.1 — the
range 1.0.0 shipped with — does not accept 0.2.0-rc.2, so 1.0.0 was silently skipped
by every 0.2 harness and 1.0.1 exists to fix exactly that. A 0.1.x harness is now
outside the declared range.
The same range is also declared as engines.dsh, which is the field a plugin market's
compatibility preflight reads and displays — dshmarket derives its verdict from that field
together with the host peers, and shows the conjunction of everything declared. Both carry
the identical string, so the market shows one requirement rather than a conjunction of two
spellings of it, and the two cannot drift apart without someone editing both.
The requirement is declared the way the ecosystem actually reads it — as
peerDependencies on the five host packages the two halves bind to:
| Peer | Why this plugin names it |
|---|---|
@deepseek-ai/dsh-session-persistence | It provides sessionPersistence, which is where the ledger's usage comes from. |
@deepseek-ai/dsh-commands | It provides commands, which registers /tokens. |
@deepseek-ai/dsh-host-webserver | It provides webServer, which serves the three settings routes. |
@deepseek-ai/dsh-client-locale | The browser half injects it for the translated labels. |
@deepseek-ai/dsh-client-ui-settings-general | The browser half injects it to add its section to the settings sidebar. |
Every one is optional, and that is deliberate rather than a hedge. DSH does not
hoist host packages into a profile's node_modules, so a required peer would report
an unmet dependency on every install and mean nothing. Optional is also what keeps the
declaration honest about this plugin's own behaviour: a profile with no web server
skips the settings page and keeps /tokens and the CLI, and a headless or TUI profile
never has the client packages at all.
What makes the declaration useful is that DSH resolves it against the install that is
actually running. Its profile checker reads a peer from the plugin's own node_modules
first, then the profile tree, then the DSH installation itself
(node_modules/dshmarket/lib/check.js), and compares the result to the range above —
so an unsupported harness is reported instead of silently mounting. Earlier versions of
this package declared dsh.compatibility.dsh instead, which nothing in DSH reads; 1.0
removed it rather than leave a field that looks like a promise and is not one.
The declarations cost a consumer nothing: optional peers are not installed, and the package still ships with no runtime dependencies and no install scripts.
The numbers are only useful if you know exactly what they count.
| Rule | Behaviour |
|---|---|
| Successful anchors only | Usage is taken from assistant/message (a completed step) and compaction/summary (one compaction call). A failed or cancelled attempt never appends either, so it is never counted. |
| Streaming samples are replaced, not added | When a step emits a usage chunk and then its final message, the final value replaces the earlier sample — one call either way. |
totalTokens is derived | It is the sum of the four buckets, never the provider's own total field. Across 15,778 real usage reports the two agreed every single time, and deriving it keeps the buckets and the total consistent by construction. |
| Reasoning tokens are a subset | Reported separately, never added into the total, because they are already inside outputTokens. |
| Local calendar days | Days are your days, not UTC days. |
| Forks are cut, resumes are not | See below. |
A stored log can begin with a prefix of history that was already recorded. Two different situations produce one, and getting them confused is the most common way a usage plugin silently gets the wrong answer:
parentSession set): the prefix is the parent's history, counted
in the parent's own log. Counting it again here would double count. It is cut.This is not a guess. Against real logs, every forked session whose parent log was still present had its pre-marker usage fingerprints contained in the parent's, while the non-forked logs carrying the same marker never repeated their prefix later in the file. On one real 139-session home the distinction was worth 79.9 million tokens of double counting that a naive "count every log" pass reports.
Works without DSH running — it reads the raw logs directly.
dsh-token-ledger [summary] [options] print the stored ledger (default)
dsh-token-ledger audit [options] recompute from raw logs and diff
dsh-token-ledger rebuild [options] recompute from raw logs
dsh-token-ledger export [options] write CSV and JSON exports
--home <path> DSH home to read (default: the DSH_HOME variable, else ~/.dsh)
--ledger <path> ledger file to read or write
--out <path> export destination directory
--days <n> days in the summary (default 7)
--models <n> models in the summary (default 5)
--write with rebuild: replace the stored ledger
--json machine-readable output
--quiet suppress the human summary, keep the exit code
Exit codes: 0 success or audit match, 1 audit found a real difference,
2 bad usage or an unreadable input. That makes it usable as a scheduled check.
A running host writes its ledger on a debounce, so a live session is routinely a
little ahead of the stored file. Reporting that as corruption would make the
audit useless. The ledger's own updatedAt settles it:
audit: match — 1 session(s) advanced after the ledger was written
(1 calls, 5100 tokens not yet flushed)
The browser half adds a Token ledger section to the settings sidebar, with three tabs: Overview, Rates and Bill.
cacheRead / (cacheRead + uncachedInput): the
share of input the prompt cache absorbed, so a route with no caching reads 0%
rather than undefined.¥0.00, and tokens the bill could
not price are named underneath the number they are missing from.0 is flagged as not necessarily
free because some platforms bill by the hour and publish no per-token price.vendor badge saying so. The rest come from a per-vendor
public dataset (models.dev); rates.source: openrouter
switches the whole table to that gateway's own quotes, which cover more models but
are not the vendors' list prices. The page says which is which, because those are
different claims.rates.vendors: 0 publishes
every vendor.rates: false makes no request at all and is a price list you maintain.The same usage, grouped the way a bill is read. Four groupings are stacked — workspace, session, model, vendor — rather than put behind a tab, because the question a bill answers is usually comparative, and each section carries its own period, so this month by workspace can sit above today by model.
cwd the ledger recorded for
the sessions that ran there; a session row reads workspace/session-name, using the
title DSH itself gave the session (falling back to the session id for sessions DSH
never titled, with the full id and path in the tooltip); a model row reads
provider/model — the route the ledger recorded, the same name the overview's model
list shows — with the price row it was billed at in the tooltip.bos, qwen-plan,
deepseek-official and zai are endpoints; deepseek, qwen and z-ai are whose
list prices their tokens carry. The row shows the provider — under the display name
the model settings page gives it, so BOS-API rather than bos, read from the
harness settings file and never written to it — and states the price source
underneath (计价来源 deepseek). Two endpoints serving the same model are therefore
two rows, which is the question a vendor bill exists to answer.其余 N 个低频会话, badged 汇总, with their columns summed). The
total does not move: the rows still add up to it. A note under the table says how many
were summarised and what the rule was, and the export always lists every session —
format=csv is the complete record. Two things are never folded: a session whose
tokens could not be priced, and a list where every row qualifies.
bill.smallSessionCost, bill.smallSessionCalls and bill.foldSmallSessions: false
change the rule or turn it off. Only the session dimension folds.TOTAL per section. There
is deliberately no grand total: the periods overlap, and adding today to this week
to this month would count the same tokens four times. The CSV starts with a UTF-8
byte-order mark, without which Excel opens it in the system code page and a session
named 编写统计 arrives as 缂栧啓缁熻; the JSON export has none, because a JSON
parser rejects a leading mark.subscriptions in the config takes monthly
plans, and the fee is spread over the days the bill covers: the 1st-to-10th of a
¥199 month is ¥66.33, not ¥199. A vendor on a plan is billed its plan and the usage
it covers is shown beside it rather than added to it, because adding them would
charge for the same calls twice. Because a plan is one charge, on a grouping where
its vendor appears in several rows — workspaces, sessions, models — the plan is
allocated across those rows in proportion to the usage it covers, which is what
makes a section's rows add up to its total. A row billed partly by plan says so
(其中套餐摊分), and a plan whose vendor has no usage in the period is still billed,
because it was still paid for. A plan's vendor may name either the provider or the
price vendor (bos or deepseek); both resolve to the usage the plan covers.The page reads three loopback-only routes: GET /api/token-ledger/summary for the
overview, GET|POST /api/token-ledger/rates for prices and the rate, and
GET /api/token-ledger/bill for the bill. The overview polls once a minute, prices
and the bill only while their view is open. All three refuse a
non-loopback peer, so they stay private even if the web server is bound to
0.0.0.0. The write half additionally requires a JSON content type — a
cross-origin form cannot send one — and caps the body at 256 KiB.
It needs a profile with a web server (the web or desktop profile). Without one,
/tokens and the CLI still work, and the section says so instead of failing. Note
that hand-entered prices are entered on that page: there is no command-line
equivalent yet, so a headless profile can read the usage ledger but cannot type a
price.
None of the three views is published through a settings namespace. That would need a schema —
a real dependency, and this package has none — and would rewrite settings.yaml on
every debounce with data that is derived and reproducible. The ledger file stays the
only store of record; prices live in rates.json beside it.
Override the composition entry by its id:
- id: token-ledger
config:
ledgerPath: 'D:/dsh/ledger.json' # default: <DSH_HOME>/token-ledger/ledger.json
backfill: false # default: true — fold stored history on startup
backfillLogs: false # default: true — read the session logs on disk too
rates: false