# dsh-turn-tokens > See **what each turn costs** and **what a request is actually made of**, right inside the DeepSeek Harness Web GUI. A host + client Web plugin. It only reads local session logs and host session projections — **no network, no API key, no telemetry**. **English** | [中文](README.md) - [What it solves](#what-it-solves) - [Features](#features) - [Installation](#installation) - [Metric definitions](#metric-definitions) - [Pricing](#pricing) - [Data sources and privacy](#data-sources-and-privacy) - [Compatibility and version requirements](#compatibility-and-version-requirements) - [Maintenance](#maintenance) - [Known limitations](#known-limitations) - [License](#license) ## What it solves The built-in meter gives you one total. This plugin answers three sharper questions: 1. **How much did *this turn* actually consume** — uncached input / cache read / output / reasoning, taken from provider-reported usage (exact), plus that turn's cost. 2. **What is one request made of** — system prompt section by section, tool schema one by one, message body by category, each with its token cost. 3. **Where did all that come from** — which prompt section or which file each system-prompt paragraph belongs to (with absolute paths), and where every user-role message that you did *not* type came from. ## Features ### The indicator next to the composer A compact chip showing `● context N%`. The dot colour is the status: **green** (<50%) / **yellow** (50–80%) / **red** (≥80%). Hover for absolute values and the window size; click to open the panel. It sits in the right-hand button row of the composer and **takes no extra line height**. ### The panel | Section | Contents | |---|---| | **Latest turn** (the first row of the panel) | Calls, the three `tokens/¥amount` buckets and the total cost of the turn that just finished. It sits at the very top so you can see at a glance what the last turn cost and where the money went; hover for its start time, peak/off-peak tier and that turn's user input | | **Session totals** | Uncached input / cache read / cache write / output, with a share bar | | **Context pressure** | Actual prompt size of the latest request, projected size of the next one, window limit, occupancy | | **Context composition** | System prompt / tool schemas / message body as three categories | | **Latest request breakdown** | Per-paragraph system-prompt cost, per-tool schema cost, message body by category | | **Per-turn table** | Calls, four token buckets and the cost of every turn, with a totals row | | **Injected context messages** | What was injected (runtime context / skill catalog / workspace instructions), from where, and whether it is still in effect | **Every paragraph and every tool row expands to the full original text** (line breaks preserved, scrollable), annotated with how its source was determined. ### Source attribution Each system-prompt paragraph is attributed in three ordered levels — first hit wins, and **nothing is guessed**: | Level | Basis | Confidence | |---|---|---| | 1 | The **section name** returned by the host's prompt-assembly API | Authoritative | | 2 | **Content matching against candidate files** (persona card, settings file, workspace instruction files in the session directory) | High (exact match after whitespace stripping) | | 3 | Paragraph text pattern → the corresponding official package path | Medium (and accepted **only if that package path actually exists**) | Injected user-role messages are recognised separately: those carrying their own source marker (e.g. `Instructions from: `) are resolved to an **absolute path**, with relative paths joined against the session working directory. A source that went through *injected → updated → removed* is grouped together, and the collapsed row shows its **latest state**. ## Installation Profile names differ per distribution; all three methods below work for both: | Distribution | Profile name | |---|---| | Native `dsh` | `web` (`dsh web` is `dsh --profile web`) | | EAC desktop client | `web-desktop` | | Prerequisite | Notes | |---|---| | Node | Shipped with DSH; nothing to install | | pnpm | **Required by method 1 only**, and it must be on `PATH` | ### Method 1: `dsh plugin` (recommended for native DSH) This package is **not published to npm**, so install it from the repository URL or a local path: ```sh # A. Install straight from GitHub dsh plugin --profile web add github:D0cx0011/dsh-turn-tokens # B. Clone first, then install from the local path (what this repo uses in development) git clone https://github.com/D0cx0011/dsh-turn-tokens.git dsh plugin --profile web add file:./dsh-turn-tokens ``` `dsh plugin` is the official CLI; it forwards the remaining arguments to `pnpm` inside the profile directory. This package declares `dsh.bundle.patch` in its `package.json`, so once the command finishes DSH **reconciles against the installed state**: a dependency that declares `dsh.bundle` is appended to the profile's `dsh.profile.bundles`, and the package's own `cordis.patch.yml` then applies as one bundle patch layer — **no manual editing of any config file**. Two known prerequisites and pitfalls: - **pnpm must already be installed.** `dsh plugin` is only a pnpm forwarder; with no pnpm on `PATH` it reports `pnpm not found on PATH` and exits 127. - **Git specs may need a build allowance.** pnpm runs a git dependency's `prepare` script, which is blocked by default and fails the install; if you hit this, add the exact key pnpm prints under `allowBuilds` in `/pnpm-workspace.yaml` and retry. This package is plain JS and has **no** `prepare` script, so it normally will not trigger; **option B (the `file:` spec) never takes this path and is the safest choice**. ### Method 2: helper script (no pnpm / offline / EAC) ```powershell powershell -NoProfile -ExecutionPolicy Bypass -File scripts/install.ps1 powershell -NoProfile -ExecutionPolicy Bypass -File scripts/install.ps1 -Uninstall ``` The script auto-detects the profile (`web-desktop`, then `web`, then the only one present); you can also name it explicitly: ```powershell powershell -NoProfile -ExecutionPolicy Bypass -File scripts/install.ps1 -Profile web ``` Target: the package is copied to `\node_modules\dsh-turn-tokens\` and the mount row is appended to that profile's `cordis.patch.yml` (**with a timestamped backup taken first**). > `powershell`, not `pwsh`: Windows PowerShell 5.1 is always present while `pwsh` (PowerShell 7) usually is not. The script is deliberately ASCII-only to dodge the encoding trap 5.1 has with non-ASCII scripts. ### Method 3: fully manual (no script at all) Equivalent to method 2, for when you want to control each step: 1. Copy this package's `package.json`, `lib\`, and `cordis.patch.yml` into `%DSH_HOME%\profiles\\node_modules\dsh-turn-tokens\` 2. Append to `%DSH_HOME%\profiles\\cordis.patch.yml`: ```yaml - insert: - id: turn-tokens name: 'dsh-turn-tokens' ``` The `cordis.yml` at the profile root says "Edit `cordis.patch.yml`, not this file" — that is always the file to edit. ### Do not mix the methods Method 1 registers the package as a **bundle layer**; methods 2 and 3 write an equivalent mount row into the **user patch layer**. Both spell the same `id: turn-tokens`, so using them together inserts the same entry twice. If you already installed manually, do not also use method 1; run `-Uninstall` first to drop the mount row. ### Verifying the install ```sh dsh plugin --profile web why dsh-turn-tokens # confirm the package is in the profile's dependencies dsh --profile web --dump-config # print the composed config tree and look for the turn-tokens row ``` On EAC, replace `web` with `web-desktop`. You can also run `node scripts/verify-install.mjs` from this repo: it checks the `package.json` contract, the mount row, and the client half. ### When changes take effect | What you changed | How it takes effect | |---|---| | **Client half** (`lib/client.js` — UI and pricing) | The host re-bundles the browser bundle: **refresh the page** | | **Host half** (`lib/index.js` — log parsing and routing) | The module is cached by the loader: **restart the DSH Web service** | After installing, **refresh the page**; if the indicator does not appear, restart the service. ### Official contracts this plugin relies on This plugin uses only DSH's public contracts. It **requires no modification to DSH's source, and no EAC-specific feature**: | Contract | Used for | |---|---| | `dsh plugin --profile …` | Official CLI, the install entry point | | `package.json` → `dsh.bundle.patch` | Read by `@deepseek-ai/dsh-app-boot` to resolve this package's patch layer | | The profile's `cordis.patch.yml` | The officially designated user patch layer | | Client slot `conversation.input.right` | Defined by the official `@deepseek-ai/dsh-client-ui-conversation` | | `tokenUsage` / `contextPressure` / `contextBreakdown` | Official session projections | If you have modified DSH's source and the indicator never appears, first use the verification commands above to confirm the mount row and the package really reached the composed config tree — **"not mounted" and "mounted but not rendered" are two different problems**, so separate them before debugging. ## Metric definitions ### Session totals and the per-turn table (exact) Accumulated per turn from provider-reported usage. The measured relationship is: ``` total = uncached input + cache read + output (reasoning excluded) ``` | Metric | Meaning | |---|---| | **Uncached input** | Input tokens that missed the prefix cache — the most expensive bucket | | **Cache read** | Input tokens served from cache. **This is the cumulative read volume across all calls, not the context size** | | **Cache write** | The portion written to cache; some providers do not report it, so it is usually 0 | | **Output** | Tokens generated by the model | | **Reasoning** | Thinking tokens. They are **billed as part of the output**, so they are **not priced separately**; the table marks them "included in output" | | **Calls** | API calls in that turn ≈ steps (one tool call ≈ one extra call) | > **Why cache read can far exceed the context window**: a single turn often makes dozens of API calls, each resending the whole prefix, and each counts as cache read. It is cumulative read volume, not context size. ### Context pressure and composition (host projections) | Metric | Precision | |---|---| | Current prompt / window limit | Exact (provider-reported) | | Projected next request | Estimate | | System prompt / tool schemas / message body | Heuristic | ### Per-paragraph and per-tool figures (parsed by this plugin) System-prompt paragraphs are split on blank lines; per-tool schema and message-body costs are estimated from characters. **Heuristic: ASCII ≈ 4 characters/token, CJK ≈ 1.5 characters/token** — use it for relative comparison and ranking only, **never as a bill**. The three-category composition total **will not** equal the exact usage above: one describes *composition*, the other *billing*. ## Pricing Every cell in the per-turn table reads `tokens/¥amount`, and the totals column gives that turn's cost. **The totals row spells out the amount for every column as well.** **Pricing follows the official page**: | Item (CNY per million tokens) | deepseek-flash off-peak | peak | deepseek-v4-pro off-peak | peak | |---|---|---|---|---| | Input (cache hit) | 0.02 | 0.04 | 0.15 | 0.30 | | Input (cache miss) | 1 | 2 | 4.5 | 9.0 | | Output | 4 | 8 | 13.5 | 27.0 | - **Peak/off-peak**: peak is Beijing time Monday–Friday 09:00–12:00 and 14:00–18:00; everything else is off-peak (off-peak is half of peak). **Each turn is priced with the tier in effect when that turn started.** - **Model aliases**: the official docs state that the legacy names `deepseek-v4-flash` and `deepseek-v4-flash-vision-exp` are still callable but **billed at Flash rates**. - **Reasoning is not priced separately** (see above). - **If a model has no rate table entry, only tokens are shown — no price.** Nothing is guessed. - **The amounts in the totals row are accumulated turn by turn**: because turns may span different peak / off-peak tiers, "total tokens × one unit price" would be wrong. Each of the uncached-input / cache-read / output columns is therefore the sum of "that turn's usage × that turn's own price", and the three columns add up exactly to the grand total in the last column. Hover the totals row to see this note in the panel. Prices are **estimates**, not your invoice. The provider reserves the right to change them — check the pricing page above periodically. ## Data sources and privacy - **Read-only**: it reads your local session logs, the settings file and a few candidate text files. It **never writes or deletes** anything it reports on. - **No network**: the plugin itself issues no requests; the pricing link in the panel is plain text. - **No upload**: no telemetry, no third-party runtime dependencies; the host half uses Node built-ins only. - **The panel shows prompt content**: to display "source" and "full text", the panel renders system-prompt paragraphs and injected message bodies. That content is already in your session — the plugin only surfaces it. ## Compatibility and version requirements **Verified environment**: DSH `0.1.2-alpha.1` (session-log format v0) and `0.1.5-rc.1` (format v3), both on the `web-desktop` profile under Windows / PowerShell 5.1. **Both log generations are supported**: from 0.1.5 onward DSH moved session logs from v0 to v3 — the filename changed from `session.jsonl.zstd` to `session.v3.jsonl.zstd`, and the system prompt moved out of `request/header.header.system` into a `system/message` event stream. This plugin handles both (the implementation tries both and uses whichever has content), so there is no switch to flip. The `log.format` and `systemSource` fields in the report tell you which one was actually read. This plugin leans on host contracts. When one of them changes, these are the failure modes — **symptom first is the fastest way to debug**: | Contract it depends on | Symptom when it breaks | Fallback today | |---|---|---| | Session log path `$DSH_HOME/sessions//session-/session[.vN].jsonl[.zstd]` | "session log not found" | Scans `session-*` directories; understands both filename generations (v0 without a version segment, vN with one) and picks the highest version | | Logs are **multi-frame** zstd | Only the earliest part is read; turns are missing | Splits frames on the zstd magic; one bad frame does not affect the rest | | Record types `session` / `turn/start` / `request/header` / `request/context` / `system/message` / `assistant/message` / `user/message` / `tool/result` | The corresponding section is empty | Every field access is guarded; missing data is skipped | | Usage field names `inputTokens` / `cacheReadTokens` / `outputTokens` / `reasoningTokens` / `totalTokens` | **Per-turn usage silently shows 0** (most dangerous) | ⚠️ None: a rename does not error, it just becomes 0 (measured unchanged in v3) | | Where the system prompt lives: v0/v1 in `request/header.header.system`, v2/v3 in the `system/message` event stream | System sections empty, or largely "unrecognised" | Tries both generations and uses whichever has content; `systemSource` reports which one was used; the event stream is replayed with surface semantics | | `request/header` shape (`tools` / `config`) | Tool list or model info empty | Existence checks (present in both generations; v3 retired `system` only) | | Session projection names `tokenUsage` / `contextPressure` / `contextBreakdown` | Corresponding figures at the top of the panel disappear (show —) | Type-checked; absent data simply is not rendered | | Slot name `conversation.input.right` | **The indicator does not appear at all** | ⚠️ None: a widget aimed at a non-existent slot is silently dropped (this slot is an official contract; see the official Web Client Slots reference) | | Client module contract (`window.__ModuleLoader__.load` / `require("react")` / `exports.inject` / `ctx.slots.register`) | Browser bundle errors, or no widget | ⚠️ None | | Prompt assembly returning `{ sections: [{ name, text }] }` | Section-name attribution degrades | ✅ Falls back to file matching and text-pattern rules | | `agent/created` payload | Only globally scoped prompt sections are visible | ✅ Falls back to the global context | | The session `cwd` field in the log | Relative paths cannot be resolved to absolute ones | ✅ Shows the declared relative path only | | Official pricing and model names | No price shown (tokens still fine) | ✅ Unmatched models show no price | | React major version | Component rendering breaks | Peer range already allows `^18 \|\| ^19` | **Suggested debugging order**: 1. Indicator missing → refresh the page, then check whether the slot name still exists 2. Panel opens but the figures are empty → check the projection names 3. **Every turn shows 0** → suspect the usage field names first (this failure is silent) 4. Some turns missing → check whether logs are still multi-frame zstd 5. Mostly "unrecognised" sources → check the prompt-assembly return shape, and whether the candidate files are readable **Adapting to an upgrade**: if the host refactors session logs or projection semantics wholesale (for example moving usage to a new record type), update the parsing section of `lib/index.js`; the UI and pricing halves are usually unaffected. **One such upgrade has already been handled**: 0.1.5 moved session logs to v3 (retiring `header.system` and adding a version segment to the filename). The changes were confined to the host half — the filename scan in `locateSession()` and the surface replay for `system/message`; the client half and the official projection contracts were unaffected. `scripts/formattest.mjs` exists for exactly this: it synthesises both log generations in a temp directory and **never touches real sessions**, so run it after upgrading DSH, then `selftest.mjs` against your real data. ## Maintenance ### Pricing Keep it in sync with the official page (link above). The edit points are in `lib/client.js`: | Location | Purpose | |---|---| | `PRICING` | The three-tier rate table (per model × peak/off-peak) | | `PRICING_ALIAS` | Legacy model name → rate table key | | `periodNow()` | Peak/off-peak decision (Beijing time) | | `cost()` / `money()` | Cost computation and amount formatting | Refresh the page after editing — the rate table lives in the client half. ### Verification scripts ```powershell node scripts/selftest.mjs # host: drives the route through a simulated host context, prints real per-turn and attribution results node scripts/formattest.mjs # both log generations (v0/v3): system carrier, filenames, surface replay node scripts/clienttest.mjs # client: simulates the module loader and slots, renders the indicator, the panel and the price column for real node scripts/verify-install.mjs # run after installing: config YAML validity + package resolution + module load ``` The fourth one matters most: **a syntax error in the profile config brings down the whole Web shell**, not just this plugin. `formattest.mjs` runs entirely inside a temporary `DSH_HOME` and **reads no real session data**, so it is safe to run first after a host upgrade. ### Layout ``` dsh-turn-tokens/ ├── package.json manifest (dsh.bundle / dsh.client declarations) ├── cordis.patch.yml self-registration patch (read by the install mechanism) ├── lib/ │ ├── index.js host half: log parsing + source attribution + HTTP route │ └── client.js client half: indicator + panel + pricing ├── scripts/ │ ├── install.ps1 install / uninstall (ASCII-only, to dodge encoding issues) │ ├── sync-profile.ps1 sync into the installed profile copy (for broken hard links; ASCII-only) │ ├── selftest.mjs host self-test (real sessions) │ ├── formattest.mjs both log generations (synthetic logs, no real data touched) │ ├── clienttest.mjs client self-test │ └── verify-install.mjs install verification ├── docs/DEVELOPMENT.md development notes: pitfalls, contract details, verification (in Chinese) ├── README.md / README.en.md └── LICENSE ``` Read `docs/DEVELOPMENT.md` before changing code — the pitfalls and the "why" live there (Chinese). ## Known limitations - Covers the **current session only**: no cross-session ledger, balance, billing or budget features. - The panel is tables and bars; there are no trend charts. - Per-turn figures rely on logs already written to disk: **the turn in progress** is only complete once that turn ends. - Without an explicit session id it picks the most recently modified session; with concurrent sessions, the last one touched wins. - System-prompt paragraphs are split on blank lines — an approximation, not the host's internal section boundaries. - Message-body estimates count every message text in the log and therefore **are not** what a single request actually carries (compaction and replacement introduce drift). - Prices are estimated from list rates and are not your invoice. ## License [MIT](LICENSE)