sandbase-harness
sandbaseai
Local-first, self-hosted AI agent runtime and MCP bridge with sandboxed sessions, memory, credentials, audit/replay, and a local Console.
PROJECT TOPICS
INSTALL REFERENCE
dsh plugin --profile web add github:Nanako660/dsh-cost-meter
该命令指向仓库当前默认分支;尚无绑定当前 commit 的完整验证结果。
PROJECT README
English | 中文
Shows this session's spend to the right of the token-usage pill in the bottom stats line, with a per-bucket breakdown on click and unit prices you configure yourself.
It is a pure consumer of @deepseek-ai/dsh-token-meter: it does not meter anything, never touches a model
request, and never writes the session log. It only multiplies the existing tokenUsage projection by the unit
prices you enter.
Once installed, that line reads:
⬤ 4 turns 68 steps · 191 tok/s ⬤ 25.7M tok · Cache hit 99% ⬤ ¥13.974
All three pills open a detail panel (the first two are identical to ui-chat's originals; the third is the per-bucket bill).
The DSH stats line (4 turns 68 steps · 191 tok/s · 5.9M tok · cache hit 98%) is rendered by
StatsPills in @deepseek-ai/dsh-client-ui-chat, which registers into the list slot
conversation.composer.dock and does exactly one thing: read two host projections.
| On screen | Source |
|---|---|
4 turns 68 steps, 191 tok/s |
the sessionStats projection (whole-log fold, dsh-session-stats) |
5.9M tok, cache hit 98% |
the tokenUsage projection (dsh-token-meter) |
The tokenUsage wire value is exactly four disjoint billing buckets:
{ uncachedInputTokens, cacheReadTokens, cacheWriteTokens, outputTokens }
where uncachedInputTokens = usage.inputTokens (cache-miss prompt input) and cache read/write stay separate.
So spend is one multiply-add:
cost = (uncached·P_in + cacheRead·P_read + cacheWrite·P_write + output·P_out) / price denominator
dsh-llm's model metadata (LlmModelInfo) carries no price field at all, and neither llm-pi-ai nor a
router catalog supplies prices — which is why the unit prices can only be user-configured, and why that is the
only thing this plugin adds.
Pick one of the two paths, do not mix them: dsh plugin add goes through pnpm, and any later
pnpm install prunes a manually copied directory as an extraneous package.
dsh plugin --profile web add github:Nanako660/dsh-cost-meter
That forwards to pnpm inside the profile directory, so $DSH_HOME\profiles\web\package.json gains a
dependency and the package (plus its @deepseek-ai/schemastery dependency) lands under that profile's
node_modules, where the loader resolves it from.
Installing the package does not compose it — pnpm never edits the composition. Append this block to
$DSH_HOME\profiles\web\cordis.patch.yml:
# >>> dsh-cost-meter
- insert:
- id: cost-meter
name: dsh-cost-meter
config:
currency: ¥
unit: per-million
input: 0
cacheRead: 0
cacheWrite: 0
output: 0
decimals: 3
showSavings: true
# <<< dsh-cost-meter
The whole config: block may be deleted — it is identical to the schema defaults and is written out only to
document the knobs.
pwsh -File install.ps1
For a machine with no npm network, or to install straight from a checkout. The script does two things, both idempotent:
$DSH_HOME\profiles\web\node_modules\dsh-cost-meter (the profile directory is the
anchor the loader resolves bare specifiers from, and the profile's own node_modules is the first stop);- insert: block above into $DSH_HOME\profiles\web\cordis.patch.yml, backing the file up to
cordis.patch.yml.bak-<timestamp> first.The web profile's patchReload: live makes the host hot-apply that row, so no restart is needed; the
browser needs one page refresh to fetch the new client bundle.
# 1. Is the row in the composed tree? (no warning, and "- id: cost-meter" present, means yes)
dsh --profile web --dump-config | Select-String cost-meter
# 2. Can the profile resolve both halves, import the host half, and validate the row config?
node test\verify-install.mjs
Note that http://127.0.0.1:3080/plugins/... cannot be used to verify: requests without the credential
from the launch URL always get a 404, including for built-in plugin bundles.
Settings → Plugins → Plugin configuration, card "花费金额(估算)" (estimated spend):
| Field | Meaning |
|---|---|
| 未命中输入 (cache-miss input) | unit price of uncachedInputTokens |
| 缓存读取 (cache read) | unit price of cacheReadTokens (the hit price) |
| 缓存写入 (cache write) | unit price of cacheWriteTokens; always 0 on DeepSeek, leave it at 0 |
| 输出 (output) | unit price of outputTokens |
| 货币符号 (currency) | display prefix, default ¥ |
| 计价单位 (unit) | per million / per thousand tokens |
| 金额小数位 (decimals) | 0–6, default 3 |
| 同时显示缓存省下的金额 | adds a counterfactual saving row to the spend panel (cacheRead × (P_in − P_read)) |
Prices persist in the cost-meter: section of ~/.dsh/settings.yaml and "重置为默认" (reset to defaults)
clears the user layer whenever you want.
When all four prices are 0 the pill reads "花费未配置" (price not configured) — with a hover hint pointing at the settings page — rather than ¥0.000. Not inventing an amount is deliberate.
# A. official path
dsh plugin --profile web remove dsh-cost-meter
# B. scripted path (also removes the marked block and the package directory)
pwsh -File install.ps1 -Uninstall
Either way, delete the marked block from the patch file by hand, then refresh the page. Removing that block immediately restores ui-chat's own stats line.
| Path | Role |
|---|---|
lib/index.js |
the only host-side responsibility: registers the cost-meter settings namespace through settings.installSection (optional attach, falling back to the composition entry when no settings provider is mounted) |
lib/client.js |
the hand-written lazy-CJS client bundle: takes over the stats cell (three pills) plus the price card |
test/smoke.mjs |
drives the real window.__ModuleLoader__ envelope as a smoke test (React is shimmed, because the deployment only installs a prebuilt frontend) |
test/verify-install.mjs |
reproduces the loader's resolution after install: both halves resolve, the host half imports, the row config passes the schema |
Why the client bundle is hand-written: @deepseek-ai/dsh-client-modules serves every package that declares
dsh.client as its lib/client.js verbatim and evaluates it in the page, while the build preset that normally
emits that envelope (packages/client/tsdown.client.ts) is not a published package — so a plugin living outside
the DSH checkout writes the envelope itself. The factory may require the shell's platform seed table:
react, react/jsx-runtime, react-dom, react-dom/client, @deepseek-ai/cordis,
@deepseek-ai/dsh-client-store, @deepseek-ai/dsh-client-ui-slots,
@deepseek-ai/dsh-client-ui-primitives, and @deepseek-ai/dsh-client-ui-dockkit. All of those are baseline
requests, so dsh.client.external declares nothing.
The two registration points (both checked against the live slot protocol):
conversation.composer.dock list, session → needs an id; this plugin uses id:"stats" + priority:-1 to shadow that cell
settings.plugin.item keyed, root → needs a key (= the "cost-meter" settings namespace)
Prices are read through the client settings scope ctx.settingsScope.bind({ namespace: 'cost-meter' }) and
subscribed with uSES; tokenUsage and sessionStats arrive through the session-scoped slot's standard
useProjection prop. Both re-render immediately, so saving prices needs no refresh.
The stats line is not the conversation.composer.dock container — it is StatsPills' own root node
(dsh-client-ui-chat). The dock's parent is InputBar.root, styled
flex-direction:column; align-items:center, so one more dock entry would simply stack below the stats line
and could never appear to the right of the token pill.
The only supported mechanism for "right of the token pill" is a list slot's shadow registration:
register({ id: "stats", priority: -1 }), where the lowest priority renders (the slot core's own validation text
reads "register at a different priority to shadow it (lowest renders)"). That cell is therefore rendered by this
plugin, and the price of that is reproducing the two original pills and their panels:
TimePill (turns/steps + tok/s) and its "会话统计" panel;UsagePill (total tok + cache-hit share) and its "Token 用量" panel;formatTokens / formatExactTokens / formatTokensPerSecond /
formatCacheHitPercent (including the binary-search rounding that refuses to round a partial hit up to 100%).Panel placement and dismissal reuse the platform seed's useAnchoredPosition / useDismissOnOutsidePointer
(with a local fallback when that export is missing), and the geometry constants and CSS are copied verbatim from
ui-chat's stat-dialog. The two pills' copy comes from the deployment's own dictionary through
ctx.locale.bind("chat"), so it follows the active language; a bundled Chinese copy is the fallback when no
locale service is mounted.
Because this plugin now owns that cell, the row is wrapped in a RowErrorBoundary: if the styled row throws, it
degrades to a plain-text stats line that re-reads the projections itself, so the stats line can never vanish
entirely.
What was reproduced, and its license, is documented in THIRD-PARTY-NOTICES.md.
This plugin's version is independent of DSH's, but it depends on DSH slot protocols, projection keys, and service names that are not stable APIs.
| dsh-cost-meter | DSH verified against | Notes |
|---|---|---|
0.1.0 |
0.1.5-rc.1 |
first release; slot protocol, the tokenUsage/sessionStats projection keys, and the settingsScope contract were all checked against that version |
peerDependencies carries that range (@deepseek-ai/dsh-settings@^0.1.5-rc.1, @deepseek-ai/cordis@^4.0.2), so a
mismatch is visible at install time.
How the version moves:
0.1.0 is not a stable release: it takes over a cell of ui-chat, so an upstream change can invalidate this port
at any time. After upgrading DSH, re-run both commands under Verify. Per-release notes are in
CHANGELOG.md.
deriveTurnTokenUsage, which carries routes).cacheWriteTokens means different things per provider: always 0 on DeepSeek, while Anthropic-style cache
writes are a separate premium tier that needs its own price.0.1.x slot/service APIs move, lib/client.js must be updated with them.StatsPills is hidden, so upstream changes to those two pills (a new bucket, new copy, a new panel row) will
not appear here automatically and must be mirrored into lib/client.js. To drop the takeover entirely, change
the registration to { name: "conversation.composer.dock", id: "cost", order: 10 } (dropping priority): the
spend pill moves to its own line below the stats line and nothing else changes.{name, version} of every live loader package with each official
API request for diagnostics, so name/version in package.json must be real and non-empty (malformed package
metadata fails request preparation outright).npm run check # node --check both halves
npm test # client bundle: envelope, takeover registration, three pills, both ported panels, spend panel, every fallback
npm run test:install # after install: both halves resolve, host half imports, row config passes the schema
smoke.mjs covers: the bundle envelope and export surface, the takeover registration's id/priority/order, the
three pills and their order, the contents of both ported panels and the spend panel, four-bucket cost arithmetic,
an empty session rendering nothing, a missing projection seat rendering nothing, the unconfigured-price state, the
error-boundary fallback, and degraded rendering with react-dom/primitives absent.
verify-install.mjs reproduces the loader's resolution (createRequire anchored at the profile manifest, matching
cordis-plugin-loader, which uses the profile directory as parentURL) and confirms that dsh-cost-meter,
dsh-cost-meter/client, and @deepseek-ai/schemastery all resolve from the profile.
cordis.patch.yml must be wrapped in - insert:. A bare - id: x is a patch targeting an
existing row of that id; when none exists it only prints patch: entry "x" not found and skips — silently
ineffective. The semantics come from applyEntryPatches in dsh-app-boot.null. A file with only comments parses as null, not as an empty list, so
-Uninstall restores the [] sentinel when it finds the document has no entries left.already has an entry with id "stats" at priority 0 — register at a different priority to shadow it. A takeover must state a different
priority explicitly.Verified locally:
dsh --profile web --dump-config reports no warning and cost-meter appears
under the user patch layer;apply / Config / the namespace constant are all
present), and the row config passes the schema;lib/client.js and lib/index.js are byte-identical to the versions in this repository that
passed the smoke test (SHA256 comparison).Not verified (needs a browser): that the running page really draws the spend pill to the right of the token pill, that all three panels open, and that the settings card saves. If that line looks wrong after a refresh, roll back by deleting the marked block as described under Uninstall — ui-chat's own stats line returns immediately.
MIT, Copyright (c) 2026 Nanako660. See LICENSE.
This package reproduces presentation code from @deepseek-ai/dsh-client-ui-chat (also MIT); the provenance and
notice are in THIRD-PARTY-NOTICES.md.
CLASSIFICATION EVIDENCE
系统优先读取 GitHub Topics,再与站内分类词典和词根规则比对。当前命中: cost-tracking。