Bundle
dsh-cost-ledger
Cross-session persistent cost ledger for DeepSeek Harness: logs every LLM token usage to SQLite and exposes record/query/budget tools. Built-in DeepSeek pricing, overridable via config.
- Source
- suimi8
- stars
- 2 stars
- License
- MIT
- Updated
- Updated yesterday
Readme
# dsh-cost-ledger
> Cross-session persistent cost ledger for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness).
> Auto-logs every LLM token-usage event to SQLite and exposes `record_cost` / `query_cost` / `set_budget` agent tools. Built-in DeepSeek pricing, overridable via plugin config. Install-and-go, no extra wiring.
## Status
**Phase 1 (host side) — complete & live-verified in DSH 0.1.0-rc.6 (last verified 2026-08-13).** 28 data-layer checks pass (`pnpm selftest`); the plugin loads and runs in a real `dsh web` profile.
The token-usage source and tool registration API are **confirmed from DSH 0.1.0-rc.6 source** and wired for real:
| Integration | API | Status |
|---|---|---|
| Token usage capture | `ctx.on('llm/stream', (options, next) => AsyncIterable<StreamChunk>)` — Cordis waterfall around every model call | ✅ wired |
| Tool registration | `ctx.tools.register(ToolDefinition)` — real `{ name, description, parameters, output:{schema,render}, execute(args, exec) }` | ✅ wired |
| Persistent store | SQLite via `better-sqlite3` (prebuilt win32 binary, no compile) | ✅ |
| Pricing | DeepSeek official (CNY/1M tok), cache-aware; config-overridable | ✅ |
| Live load in `dsh web` | installed via `dsh plugin --profile web add .`, host loads the bundle, `apply()` runs, SQLite opens | ✅ verified |
| End-to-end token capture | `dsh --profile headless "..."` → real LLM call → `llm/stream` listener fires → usage chunk harvested → SQLite row written | ✅ verified |
**Live-verified:** a one-shot `dsh --profile headless "reply with exactly: hi"` produced real rows in `ledger.db` (`{model:"glm-5-2", inputTokens, outputTokens, cacheReadTokens}`) — the full `llm/stream` → usage-chunk → SQLite pipeline works against the real host.
Module loading uses `.ts` extension specifiers (`./store.ts`) — the Cordis loader rewrites these and loads TypeScript directly, so **no build step** is needed; the plugin runs from source after install.
Two issues found & fixed during live integration:
- **`{ global: true }` on `ctx.on('llm/stream')`** — without it the listener only sees calls from its own fiber; the agent loop dispatches from a different scope. (Mirrors the host's own `invariant.js` usage.)
- **No uninjected service access** — `ctx.session`/`ctx.workspace` throw "cannot get property without inject" unless declared in `inject`. Session id now comes from the `llm/stream` event's `options.sessionId` instead.
**Phase 2 (WebUI dashboard panel) — researched, not implemented.** See [Phase 2](#phase-2-webui-panel).
---
## What it does
- **Auto-logs token usage**: subscribes to the `llm/stream` waterfall and writes `{timestamp, session, project, model, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cost}` per real provider call (including retries).
- **Computes cost**: built-in DeepSeek pricing (`deepseek-chat` / `deepseek-reasoner`, CNY per 1M tokens). `inputTokens` is uncached input; billed input = `input + cacheRead + cacheWrite`, each priced at its own rate. Override or add models via config.
- **Three agent tools**:
- `record_cost` — manually record a usage entry (backfill/test).
- `query_cost` — total spend + per-model breakdown within a range (`today`/`week`/`month`/`all`/ISO); reports current daily-budget status.
- `set_budget` — create/update/remove a spending budget (`daily`/`weekly`/`monthly`/`never` window, label-scoped: `daily`, `model:<name>`, `project:<name>`).
## Install (community / standard)
```powershell
dsh plugin --profile web add <path-or-package>
```
The host discovers the plugin from `package.json` (`dsh.bundle.patch`) — no absolute path, no manual wiring. The `prepare` script builds `lib/` from source, so a git checkout installs self-contained. Restart `dsh web` after install.
> **Git install note:** pnpm ≥10 blocks a git dependency's `prepare` script until allowed. If the first `add` fails, copy the exact package key pnpm printed into the profile's `pnpm-workspace.yaml` (`allowBuilds: dsh-cost-ledger: true`) and re-run — exactly as the official docs describe.
## Local development
```powershell
pnpm install
pnpm selftest # verify the data layer (28 checks)
pnpm typecheck
pnpm build # build host (lib/*.js) + client (lib/client.js)
# load into a running dsh web via the patch (absolute path, this machine):
dsh web --patch ./cordis.patch.yml
```
## Pricing — built-in official rates
Built-in model pricing (元/百万 tokens, from each platform's official pricing page):
- `deepseek-v4-flash` — input 0.10 / output 3.0 / cacheRead 9.0 (peak tier)
- `deepseek-v4-pro` — input 0.30 / output 9.0 / cacheRead 27.0 (peak tier)
- `deepseek-chat` (V3) / `deepseek-reasoner` (R1) — legacy aliases
- `glm-5-2` — default 0 (override in the dashboard ⚙️设置 tab or config; fill from Zhipu's pricing)
Unknown models record tokens only (cost = 0) until you set a price. Override anytime via the **⚙️设置 tab** in the dashboard or the WebUI settings card — changes apply to new calls immediately.
## Config
Edited in the dashboard **⚙️设置** tab (runtime, not persisted across restart) or under the WebUI plugin config card (durable) / `cordis.patch.yml`:
```yaml
config:
dbPath: 'dsh-cost-ledger/ledger.db' # relative to DSH cwd
defaultDailyBudget: 0 # CNY; 0 = no limit
pricing:
deepseek-chat: # override or add any model
input: 2
output: 8
cacheRead: 0.5
cacheWrite: 8
```
## Architecture
```
src/
pricing.ts built-in DeepSeek price table + computeCost()
store.ts SQLite ledger: insert + summaryByModel + spentSince + budgets
tools.ts three ToolDefs (pure handlers over store + config)
config.ts Schema (schemastery) config
index.ts apply(ctx): llm/stream listener + registerTools + cleanup
client/ Phase 2 WebUI entry (stub)
cordis.patch.yml local-dev profile patch (absolute path)
scripts/selftest.ts standalone data-layer verification
```
### Token capture (confirmed API)
`llm/stream` is a Cordis **waterfall** wrapping every streaming model call (retries included). The listener transparently forwards the underlying stream while harvesting the `usage` chunk:
```ts
ctx.on('llm/stream', (options, next) => (async function* () {
let usage: TokenUsage | undefined
for await (const chunk of next()) {
if (chunk.type === 'usage') usage = chunk.usage
yield chunk
}
if (usage) record(options, usage) // persist one row
})())
```
Key billing facts (from official `TokenUsage`):
- `inputTokens` = uncached input only. Billed input = `input + cacheRead + cacheWrite`.
- `reasoningTokens` is already included in `outputTokens` — never double-count.
- `usage` is not guaranteed (early abort/error) — absence is logged at debug.
The `TokenUsage` / `StreamChunk` / `GenerateOptions` types live in `@deepseek-ai/dsh-llm` (an unpublished host-injected package). This plugin declares minimal local types to typecheck standalone; swap for a real `import type` once the host package is resolvable.
## Phase 2 (WebUI panel)
Researched against the `dsh-web-ui` reference. Key finding: **DSH exposes no general-purpose sidebar-panel slot** for external plugins. `sidebar.workspaces` / `sidebar.settings` are single-occupant and taken by the shell. Options:
- **DOM-level bypass** (the `dsh-task-board` / `dsh-ssh` approach): `MutationObserver` + `createRoot` into `[data-pane="sidebar"]` / `[data-pane="conversation"]`. Most flexible.
- **Settings card** via `web-ui.plugin.item` (lightest — a summary card under Settings → Plugins).
- **No chart-library precedent** in the ecosystem. Recommended: inline SVG, or vendor `recharts` (strict CSP, no CDN).
## Compatibility
- **DSH version:** verified against `0.1.0-rc.6` (the API surface — `ctx.on('llm/stream')`, `ctx.tools.register`, `ctx.inject`, `agentDefaultModel` — is confirmed from DSH 0.1.0-rc.6 source).
- **Last verified:** 2026-08-13.
- **Profiles:** loads under both `dsh web` (full UI + HTTP API + dashboard) and `dsh --profile headless` (token capture + tools only; HTTP API and dashboard are web-profile-only and silently skipped).
- **OS:** the SQLite backend ships a prebuilt win32 binary via `better-sqlite3`; other platforms build from source at install time (requires a C++ toolchain). Verified on Windows 11 / Node 22.
- **Pre-release caveat:** DSH is in developer preview with expected compatibility-breaking changes. The token-usage source and tool registration API are confirmed against rc.6; a future mainline may rename them, which would require a plugin update.
## Install
```powershell
dsh plugin --profile web add <path-or-package>
```
The host discovers the plugin from `package.json` (`dsh.bundle.patch`) — no absolute path, no manual wiring. The `prepare` script builds `lib/` from source, so a git checkout installs self-contained. Restart `dsh web` after install.
> **Git install note:** pnpm ≥10 blocks a git dependency's `prepare` script until allowed. If the first `add` fails, copy the exact package key pnpm printed into the profile's `pnpm-workspace.yaml` (`allowBuilds: dsh-cost-ledger: true`) and re-run — exactly as the official docs describe.
## Uninstall
```powershell
# Remove the bundle from the profile, then restart dsh web
dsh plugin --profile web remove dsh-cost-ledger
```
This removes the bundle from the profile's `dsh.profile.bundles` list and unlinks the package. The ledger database (`dsh-cost-ledger/ledger.db`) and any config overrides in `cordis.patch.yml` are **not** removed automatically — delete them manually if you want a clean sweep:
```powershell
Remove-Item -Recurse -Force dsh-cost-ledger # the data dir created under the DSH cwd
```
To disable temporarily without removing: comment out `dsh-cost-ledger` in the profile's `cordis.yml` `bundles` list and restart.
## Quick start
```powershell
# 1. Install into your web profile (see Install above)
dsh plugin --profile web add .
# 2. Restart dsh web, then just use the agent normally
dsh web
```
Every model call is now auto-logged. After a few prompts, ask the agent to query spend:
```
> 查一下今天花了多少钱(query_cost today)
```
Or open the dashboard panel in the Web UI (the cost-ledger tab) to see a live summary, per-model breakdown, and the budget/budget settings — no extra wiring.
## Permissions & data
- **Files written:** one SQLite database at `dsh-cost-ledger/ledger.db` (path configurable via `dbPath`, relative to the DSH working directory) plus its WAL/SHM sidecar files. Nothing else touches the filesystem.
- **Network:** none. The plugin makes no outbound network calls. (The `parse-prices` endpoint calls the **host's** own `ctx.llm.stream()` — it reuses the model call path you already configured, not a new connection.)
- **Credentials:** none read, stored, or transmitted. The plugin never touches your API keys; it only reads token counts and model names that the host already emits on the `llm/stream` event.
- **Data logged per model call:** `{timestamp, sessionId, project, model, provider, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cost}`. No prompt content or completions are ever stored — only aggregate token counts and metadata.
- **HTTP API:** when running under `dsh web`, the plugin registers read-only-ish JSON endpoints under `/api/cost-ledger/*`. They bind to the host's web server and are reachable from the same origin as the Web UI. The `cleanup` and `parse-prices` endpoints are write/POST endpoints intended for the dashboard UI.
## Troubleshooting
- **`Cannot find module './store.ts'` / load error after a git pull:** the Cordis loader resolves `.ts` specifiers directly, but a stale `lib/` can interfere. Run `pnpm build` (or `pnpm install` to trigger `prepare`) and restart `dsh web`.
- **`better-sqlite3` install fails on macOS/Linux:** the prebuilt binary is win32-only; other platforms compile from source. Install a C++ toolchain (`build-essential` / Xcode CLT) and re-run `pnpm install`.
- **No cost rows appear:** confirm the model call actually emitted a `usage` chunk (early aborts/errors may not). Token capture is best-effort — absent usage is logged at debug level. Also confirm `dbPath` is writable.
- **`parse-prices` returns 503 / "default model not configured":** the `agentDefaultModel` service hadn't resolved yet, or no default model is set in Agent settings. Set a default model and retry.
- **pnpm blocks the `prepare` script on git install:** add `allowBuilds: dsh-cost-ledger: true` to the profile's `pnpm-workspace.yaml` and re-run `dsh plugin add`.
- **Logs:** plugin diagnostics go to the host log (`dsh web` console / `dsh-run.log`); the SQLite ledger itself is the system of record for cost data.
- **Rollback:** `dsh plugin --profile web remove dsh-cost-ledger` + restart; optionally delete the `dsh-cost-ledger/` data dir (see Uninstall).
## Development
```powershell
pnpm install
pnpm selftest # verify the data layer (28 checks)
pnpm typecheck
pnpm build # build host (lib/*.js) + client (lib/client.js)
# load into a running dsh web via the patch (absolute path, this machine):
dsh web --patch ./cordis.patch.yml
```
`react` / `react-dom` and the `@deepseek-ai/dsh-client-*` runtime packages are **external** to the client bundle (resolved by the host's module loader at runtime), so they are correctly declared as `devDependencies` / `peerDependencies`, not `dependencies`. The only runtime `dependency` is `better-sqlite3`.
## License & security
MIT — see [LICENSE](LICENSE).
**Security reporting:** this plugin has no network surface and stores no credentials, but if you find a vulnerability (e.g. unsafe SQL handling, path traversal via `dbPath`), please **do not open a public issue**. Report it privately via [GitHub Security Advisories](https://github.com/suimi8/dsh-cost-ledger/security/advisories/new) (Security → Report a vulnerability on the repo). All SQL uses parameterized statements (`@named` bind params) and all filesystem access is confined to the configured `dbPath`.
Install
dsh plugin --profile web add github:suimi8/dsh-cost-ledger
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-cost-ledger from the hub
- This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.