Bundle
dsh-get-balance
DeepSeek balance & token-cost plugin for DeepSeek Harness (dual-face): query official balances of every DeepSeek provider (multi API key), compute costs (last question / session / today-project / today-all) from token usage with a user-editable price table. Unified modal UI, bilingual (zh/en)
- Source
- jsoncode
- License
- MIT
- Updated
- Updated 6 days ago
Readme
# dsh-get-balance
A balance & cost plugin for DeepSeek Harness:
- **Multi-account balance** — enumerates every DeepSeek provider (pi-ai routes / official route / extra keys), folds the same account into one row, and shows every account balance at a glance;
- **Real-time stats** — live per-session token usage and cost estimation, with online-editable price tiers (model × peak/off-peak);
- **Bilingual UI** — copy follows the host language (Simplified Chinese / English);
- **Friendly & simple** — one unified modal (Balance / Cost / Prices) plus a sidebar entry and a live session-header button; intuitive and ready to use.

[Simplified Chinese](README.zh-CN.md) · [Screenshots](preview.md)
## Features
### Balance tab
- Enumerates every DeepSeek provider:
- entries from the host `llm-pi-ai` settings whose `baseURL` points at DeepSeek,
- the official `llm-deepseek` route,
- manually attached **extra API keys** (see below).
- Each API key row shows "**Today spend ≈xx CNY | Balance xx CNY**"
(numbers green) — the key's own today cost (matched from the per-key cost
stats by provider route, ≈0.00 when unused) plus its account balance.
- **One row per account**: when several routes resolve to the *same* API key
(e.g. a pi-ai route named `deepseek` derives the credential ref
`DEEPSEEK_API_KEY`, colliding with the official `llm-deepseek` default),
they are folded into a single row — the row keeps its first entry and shows
a chip for every route sharing the key (tooltip: "shares API key with …").
The balance query runs once per distinct key, so the same account is never
displayed twice with different names.
- For each provider the plugin resolves the configured `apiKeyEnv` via the host
`credentials` service and calls the official
`GET https://api.deepseek.com/user/balance` (proxied by the host — keys never
reach the browser). Each row shows `total_balance` / `granted_balance`
(+ topped-out flag) or the failure reason. Results are cached in memory for 60s;
"Refresh" bypasses the cache.
- **Extra API keys**: keys outside any providers config can be attached directly
from the modal (label + key, masked echo), persisted to
`$DSH_HOME/dsh-get-balance.json`.
### Cost tab — per-API-key breakdown
- One **table**: columns *token / category / input (miss) / input (cache hit) /
output / hit rate / est. cost*. Every API key is a group of four rows
(**Last question / This session / Today·project / Today·all**) with the token
cell merged across them (label + masked key + official/non-official chip);
a **Total** group comes first, key groups follow sorted by token volume.
Numbers are compact (K/M/B/T/P); hit rate = cache-hit ÷ all input-side tokens.
- **Token usage is counted per key regardless of officialness**; **cost is only
computed for official keys** (API domain `api.deepseek.com`) — non-official keys
show a "not billed" chip instead of an amount. Multiple official keys (several
routes pointing at the official API) each get their own group and their own bill.
- **Multi-provider sessions are strictly split per provider**: token stats and
cost estimates follow each request's own `request/context` provider, and each
group carries a source chip (pi-ai route / official route) plus the
official/non-official chip (alias routes are judged by baseURL hostname, so a
pi-ai route pointing at `api.deepseek.com` bills as official), so providers
never mix. The **Last question** entry prices every sample with its own model
(per-sample model from `request/context`), not a session-wide model.
- **Aligned with the balance tab**: every configured provider is listed as a
group whether or not it has usage — providers without usage (or without a
credential) show zero tokens and a "—" amount.
- Official detection: the `provider` field of `request/context` events → that
provider's baseURL in host settings → hostname equals `api.deepseek.com`
(trailing slash / case normalized; lookalike domains such as
`api.deepseek.com.xx.com` are not official).
- Last turn / session: in-memory event folding (same (turn,step)
last-value-wins semantics as the official `tokenUsage` projection). Today
entries: on-demand scan of `dshHomePath('sessions')` logs (`.jsonl` and
`.jsonl.zstd`, frame-wise zstd decode, per-file memoized).
### Price settings tab — platform sub-tabs + official pricing-table layout
- **Platform sub-tabs** (currently DeepSeek only; more providers' pricing can be
added later as additional sub-tabs).
- Mirrors the official price table layout minus the category column:
`Model version` (colspan=2) + one column per model; three metric groups (input
cache-hit / cache-miss / output, each rowspan=2) + off-peak/peak rows;
**only the price cells are input boxes** (peak red, off-peak green).
- Each model has **peak and off-peak** sets of four prices (per million tokens:
input / cache read / cache write / output, CNY).
- **Periods are configurable**: peak windows (cross-midnight supported) + a
**timezone-offset slider** (UTC-12..+12, shown as UTC±0 / UTC+8 / UTC-5)
+ a **"weekends half price" toggle** (Saturdays & Sundays excluded from peak
windows and billed at off-peak rates all day).
Official default: Beijing 9:00–12:00 & 14:00–18:00 are peak; off-peak = peak × 0.5.
- Built-in fallback is the official V4 tiers (`deepseek-v4-flash` /
`deepseek-v4-pro` / `deepseek-v4-flash-vision-exp`). Old flat-format
config migrates automatically on first read (legacy built-in defaults upgrade to
the official three tiers).
### Session-header live button
- Registered on `conversation.session.header.utilities`, showing
**Session xxM | ≈¥xx** (both green, digits roll vertically odometer-style) —
the current session's total tokens (compact K/M/B/T/P format) and its
estimated cost.
- **Clicking the button refreshes once**; auto-refreshes at the configured interval;
refreshes on session switch; and refreshes the moment each AI request completes
(the host session snapshot gains a new `assistant` message node — request-level,
not per streamed token), so a turn with several requests updates after each one.
Only completions that hit the official DeepSeek API (api.deepseek.com) also
force-refresh the footer balance (bypassing its 60s cache) — requests to
non-official endpoints update tokens & cost only, without a balance query.
- **Per-provider breakdown popover**: since a session may switch providers
mid-way, the button shows the **merged** totals; clicking opens a bubble
popover listing each provider's own stats
(`ds-self 268K | ≈¥0.41`, non-official rows show "not billed").
### Entry button (sidebar footer)
- `sidebar.footer.action` **Balance** button: the label and a period dot on the
left, the amounts right-aligned ("Balance ¥110.00 | ¥99.50") — currency symbol
prefix, digits in green and rolling vertically odometer-style on change.
**One segment per provider (account), separated by `|`**; an account whose
balance could not be fetched (no API key configured / query failed) shows a
**red `--`** placeholder (hover shows the reason). The period is a
small dot (**red** for peak hours / **green** for
off-peak, half price) whose hover tooltip shows the full info
"Currently peak hours · full price billing" / "Currently off-peak hours · half
price billing" (full price in red / half price in green); the amounts come from
the balance API.
### Auto refresh
- An **Auto** button (left of the Refresh button in the modal header) opens a
config dialog: set the interval (seconds) → Start/Stop. While running, the input
and the Start button are disabled; stopping re-enables them.
- At each interval, balance & cost refresh automatically (the modal when open, the
header button otherwise). The interval is persisted to
`$DSH_HOME/dsh-get-balance.json` and survives restarts.
### Config / packaging
- Schemastery `Config` (no required options) + a plugin-owned config file
`$DSH_HOME/dsh-get-balance.json`: extra keys, price config and the auto-refresh
interval are persisted there (JSON, versioned), never into the host's
`settings.yaml`. Existing data stored in the old `dsh-balance` settings
namespace is migrated automatically on first run.
- `dsh.bundle` + `dsh.client`(web) manifests; the official
`deepseek-harness` project is never modified — everything rides existing slots
and the HTTP / command channel.
### Plugin config file
- Location: `$DSH_HOME/dsh-get-balance.json` (next to `settings.yaml`).
- Contents (all fields optional on read; invalid values fall back to defaults):
```json
{
"version": 1,
"extraKeys": [ { "id": "k1", "label": "main", "apiKey": "sk-..." } ],
"prices": {
"tiers": [
{ "id": "deepseek-v4-flash", "name": "deepseek-v4-flash", "currency": "CNY",
"match": "deepseek-v4-flash",
"peak": { "input": 3.0, "cacheRead": 0.10, "cacheWrite": 0, "output": 9.0 },
"offPeak": { "input": 1.5, "cacheRead": 0.05, "cacheWrite": 0, "output": 4.5 } }
],
"timezoneOffsetMinutes": 480,
"peakWindows": [ { "start": "09:00", "end": "12:00" }, { "start": "14:00", "end": "18:00" } ],
"weekendOffPeak": false
},
"autoRefreshSeconds": 0
}
```
- It is read on every query and written atomically (temp file + rename) on save,
so **hand-editing takes effect immediately** (no restart). A corrupted file is
renamed to `dsh-get-balance.json.bak-<timestamp>` and defaults are used.
## Layout
```
├── src/host/*.ts # host half: index.ts, providers.ts (enum + official check),
│ # balance.ts, cost.ts (fold + today scan + period pricing +
│ # official filter), ops.ts, config-file.ts (plugin config
│ # file read/write + legacy settings migration),
│ # fence.ts, types.ts
├── src/client/* # browser half: plugin.tsx (slots + timer), BalanceModal.tsx,
│ # HeaderButton.tsx, FooterButton.tsx, rpc.ts, store.ts,
│ # i18n.ts, styles.ts, logo.ts
├── lib/index.js # host bundle (tsdown ESM), committed for git installs
├── lib/client.js # browser bundle (__ModuleLoader__ factory), committed
├── lib/types/ # declarations (tsc -b)
├── scripts/ # verify-client.mjs
├── tsdown.config.ts # tsdown config (host + client banner wrap)
├── tsconfig.json # solution: tsconfig.host.json / tsconfig.client.json
├── cordis.patch.yml # bundle patch
├── package.json # dsh.bundle + dsh.client(web) manifests + peerDependencies
├── README.md # this file (English, default)
└── README.zh-CN.md # Simplified Chinese docs
```
## Install
```sh
# published: npm / tarball / GitHub
dsh plugin --profile web add dsh-get-balance
dsh plugin --profile web add ./dsh-get-balance-0.1.0.tgz
dsh plugin --profile web add github:you/dsh-get-balance#<sha>
dsh --profile web --dump-config # inspect the plugin layer
dsh --profile web # start
```
No static config required: extra keys, price config and the auto-refresh interval
are edited in the modal and persisted to `$DSH_HOME/dsh-get-balance.json`.
## Release
Toolchain: **tsc + tsdown** (no vite): `tsc -b` type-checks and emits
declarations; `tsdown` (Rolldown core) bundles the host half
(`lib/index.js`, ESM) and the browser half (`lib/client.js`, single-file CJS
`__ModuleLoader__` factory). Dependency manager: **pnpm 10**.
```sh
pnpm install # per pnpm-lock.yaml
pnpm run build # clean lib → tsc -b (types + declarations) → tsdown (both halves)
pnpm run verify # simulate the host seed to validate lib/client.js (optional)
pnpm publish # or pnpm pack / git push origin main (lib/ committed → git installs need no build)
```
### Auto publish (GitHub Actions)
Pushing a `v*` tag (`pnpm run release` bumps the patch version, rebuilds and
tags) triggers [`.github/workflows/publish.yml`](.github/workflows/publish.yml):
- **release job**: Setup Node → `pnpm install --frozen-lockfile` →
`pnpm run check` → `pnpm run build` → `pnpm pack` → create GitHub Release;
- **publish-npm job**: publish to npm — requires the `NPM_TOKEN` repo secret.
## Development
Requirements: **Node ≥ 26 + pnpm 10** (pinned via the `packageManager` field).
```sh
pnpm install # devDependencies: typescript, tsdown, @types/react, @deepseek-ai/* type packages…
pnpm run check # full-tree TypeScript check (tsc -b)
pnpm run build # rebuild both bundles after source changes (tsc -b && tsdown)
pnpm run verify # validate lib/client.js against a simulated host seed
```
Attach the local checkout to a dsh instance (from the plugin repo):
```sh
cd dsh-get-balance
dsh plugin --profile web add ./
```
> The host loads `index.js` as native Node ESM, so
> `@deepseek-ai/schemastery`, `@deepseek-ai/dsh-tools`,
> `@deepseek-ai/dsh-settings`, `@deepseek-ai/dsh-home-paths` must resolve from
> the plugin directory (run `pnpm install` there; `node_modules` is gitignored).
> Host-half (`src/host/`) changes need a **dsh restart**; browser-half
> (`src/client/`) changes apply on a **page refresh**.
- Host half lives in `src/host/`; browser half in `src/client/`;
- The `window.__ModuleLoader__.load` factory wrap of `lib/client.js` is generated
by tsdown's banner/intro/footer; external deps (`react` etc.) resolve through the
host module table (seed) at runtime.
## Implementation notes
- **Browser ↔ host**: HTTP route `/dsh-balance/api` (POST JSON, host
`webServer` + trust fence) with a `ctx.remote.commands.execute` fallback;
errors carry a `code` that the client localizes.
- **Credentials**: the `credentials` service is looked up lazily per request
(not captured at apply time), avoiding a "no credential" state when the host
service starts late; the providers op returns `credentialsPresent` and per-entry
`keySource` diagnostics.
- **Pricing**: `(uncachedInput × p_input + cacheRead × p_cacheRead +
cacheWrite × p_cacheWrite + output × p_output) / 1e6` per million tokens, priced
by each event's time (peak/off-peak).
- **Official filter**: `request/context` `provider` → baseURL in host settings →
hostname == `api.deepseek.com`; non-official tokens are counted only
(per-provider four buckets), never billed.
- **Today aggregation**: `dshHomePath('sessions')/<projectKey>/<sessionId>/session.jsonl(.zstd)`,
decoded frame-wise via `zstdDecompressSync` from `node:zlib`.
- Peer deps (`@deepseek-ai/cordis`, dsh-tools, schemastery, dsh-settings,
dsh-commands, dsh-session, dsh-api-remotes, client runtime / ui-slots /
ui-settings / cordis-client-runner, `react`) are resolved by the host at install.
- The official `deepseek-harness` project is **never modified**; everything uses
existing slots (`sidebar.footer.action`, `shell.overlay`,
`conversation.session.header.utilities`) and the HTTP / command channel.
Install
dsh plugin --profile web add github:jsoncode/dsh-get-balance#8840e33e6396fd92e10fe940841a4e828e9955d7
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-get-balance 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.