Skip to content

About

DeepSeek Harness conversation usage statistics plugin: runtime split, token trend, tool ranking, turn health, agent tool

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

@echovic/dsh-usage-stats

CI Release License: MIT

Conversation usage statistics for the DeepSeek Harness Web shell — a central panel (and a matching agent tool) that answers how much did my conversations cost, where did the time go, and what is trending. Per-model token accounting, an automatically fetched official price table (with peak/off-peak billing and a monthly budget), Search/Fetch per-request costs, and a per-conversation detail drawer are part of the surface.

用量统计面板:总览卡片、Token 趋势、运行时长构成、Token 构成、工具消耗排行、轮次健康

(截图来自 v0.2;v0.3 在 Token 构成下方新增模型用量表,卡片区新增预计费用,点击任意对话行打开详情抽屉。)

What it measures

Every figure is folded from the durable Session event log — the only source of truth — and deliberately mirrors the accounting the Harness itself shows, so the panel cannot disagree with a conversation's own usage pill:

Figure Definition
Token buckets assistant/message / assistant/attempt usage, with a same-turn/same-step sample replacing the earlier one and llm/retry-started closing the replacement slot, so retried attempts add their own consumption. inputTokens is the uncached prompt bucket (the tokenUsage projection's convention); cache read/write are separate buckets.
Total tokens uncached input + cache read + cache write + output
Cache hit rate cache read ÷ prompt-side total
Runtime wall-clock turn/start → turn/end spans, clipped to the selected range; an in-flight live turn counts up to now
Model time step/start → assistant/message (a cancelled step contributes none)
Tool time tool/call → matching tool/result by call id, as an interval union so parallel calls are not double counted
Human wait approval/asked → approval/decided plus the whole duration of ask_user_question calls; approval waits that sit inside a tool interval are moved from tool time to human time
Other the remainder after the three segments — queueing, retry backoff, gaps
Steps / turns one per step/end; turns counted per turn/start
Tool ranking per call id: execution time (minus human wait), calls, error results
Turn health turn/end.reason histogram plus llm/retry count
Trend turns bucketed by end time into local days (all/30d/7d) or hours (1d)

The four runtime segments never overlap and always sum to the runtime: the fold keeps each interval list and the report takes interval unions, because tools run in parallel and an approval wait sits inside the tool call that asked for it.

Forked conversations: a fork's inherited prefix was consumed in the parent, so it is excluded from every total by default (the row still shows it as 继承 X, and the "计入分支继承" switch includes it). A resumed (not seeded) session folds its whole log — that prefix is its own history.

Cost and budget (official prices, auto-fetched)

The Harness log carries token counts, not money. DeepSeek publishes list prices on its docs page, so the Host half fetches and parses that page by default — no hard-coded price table is shipped as truth:

live page (api-docs.deepseek.com)
   └─ success → cached on disk for 24 h
   └─ failure → last cache → bundled snapshot (official-prices.json)
user overrides ($DSH_HOME/usage-stats/prices.json)  ← optional, wins per entry
  • Currencies: the CNY page (/zh-cn/..., prices in 元) is the default; set DSH_USAGE_STATS_CURRENCY=USD (or "currency": "USD" in the user file) to use the USD page. Prices are per 1M tokens, per bucket (input, cacheRead, cacheWrite, output); cache writes are billed at the cache-miss rate.
  • Refresh: the table is re-fetched when the 24 h TTL expires, or immediately when the panel's Refresh button (or the footer's 刷新官网价格 / Refresh official prices action) is used (?force=1); the header Refresh tooltip says so too. DSH_USAGE_STATS_AUTO_PRICING=0 disables the automatic fetch entirely.
  • Provenance is reported: the payload says whether the table came from the live page (auto), the TTL cache (cache), the bundled snapshot (bundled), a manual file (manual) or a mix (mixed), with its URL and fetch time. The panel footer shows that line; the agent tool reports it too.
  • Failure never breaks the panel: an unreachable page falls back to the cache/snapshot with a warning; a malformed user file surfaces a parse error and is ignored.

Peak and off-peak billing

The official page publishes two tiers, and the plugin applies them per usage sample by the sample's timestamp:

Tier Official Chinese-time window UTC window (Mon–Fri)
PEAK weekdays 09:00–12:00 and 14:00–18:00 01:00–04:00 and 06:00–10:00
OFF-PEAK everything else (weekends included) everything else

Off-peak is half the peak price (the official page states this). Each model's token buckets are split into peak/off-peak while folding the log, so the cost follows when the work actually ran rather than when the panel was opened. Chinese statutory holidays are not modeled — the classifier only knows weekdays — which is the one deliberate approximation here.

Search & Fetch per-request billing

The official page prices the standalone Search and Fetch APIs per 1,000 successful requests (currently ¥5 / $0.75 each). The plugin counts them from the session log and includes them in the cost, month-to-date and budget:

Service Counted from Billed?
Search web/deepseek-search-llm-request events — one outbound Search API request each (a web_search call can issue 2–4), counted only when that call's tool result succeeds yes, official rate per 1,000 requests
Fetch web_fetch tool calls no by default — the shipped web-fetch-http provider performs a local HTTP fetch, not a Fetch API request

The user file can override the counts' prices or opt Fetch into billing for a composition that uses a billable Fetch provider:

{
  "services": {
    "search": { "pricePerThousand": 5, "billed": true },
    "fetch": { "pricePerThousand": 5, "billed": true }
  }
}

Requests are attributed to the most recent open web_search call and are only billed when its tool result reports success. A failed call's requests, a call that never produced a result (interrupted turn), and requests with no matching call are excluded. Remaining approximation: a call that fails late may have already issued successful requests that the fold drops (it under-counts instead of over-counting); per-request success is not recorded in the log.

Optional user overrides and budget

cp prices.example.json ~/.dsh/usage-stats/prices.json   # optional
# or point the plugin elsewhere:
#   DSH_USAGE_STATS_PRICES=/path/to/prices.json
{
  "auto": true,
  "currency": "CNY",
  "budget": { "monthly": 100 },
  "prices": {
    "deepseek-official/deepseek-v4-pro": { "input": 9, "cacheRead": 0.3, "cacheWrite": 9, "output": 27 }
  }
}
  • Entries in prices override the official table per key (a flat bucket set applies to both tiers; { "peak": {...}, "offPeak": {...} } sets them separately). Wildcard precedence is provider/model → provider/* → */* → *.
  • budget.monthly (same currency) drives the 80% warning / 100% over-budget banner and the month-to-date figure, which is computed from the whole corpus regardless of the panel's selected range.
  • The 区间费用 / Range cost card is the estimated cost of the selected range (it changes when the range tab changes); the month-to-date figure and budget progress stay in its hint, and the budget alerts remain month-based.
  • Partial cost is explicit: a model without a price still reports tokens; it is listed in costing.unpricedModels, the footer says how many are unpriced, and its row cost shows — instead of a fake zero.

Architecture

package.json          dsh.bundle.patch + dsh.client { platform: web }
cordis.patch.yml      inserts the one Loader row ("usage-stats")
lib/aggregate.js      pure folds: events → per-turn records → range report
lib/pricing.js        official-page parser, cache, bundled fallback, user overrides
official-prices.json  bundled snapshot of the official table (offline fallback)
lib/index.js          Host half: scans the corpus, serves the route, registers the tool
lib/client.js         browser half: sidebar rail icon + central "main" panel
test/                 aggregate, route and browser-composition tests

Host half (lib/index.js):

  • enumerates the logical corpus with ctx.sessionQuery.listSessions();
  • folds live sessions from memory (ctx.sessions.get(id).snapshotEvents()), persisted sessions through sessionQuery.readSession(id);
  • reuses a persisted session's per-turn analysis until its sessionPersistence revision changes, and serves the whole corpus from a short-lived TTL cache (15 s; DSH_USAGE_STATS_TTL_MS overrides it) with single-flight behind it. Range/inherited switches re-assemble the report from the cached corpus without re-reading logs;
  • registers GET /api/usage-stats.summary through ctx.connection.fetch.register, inside the connection service's browser authentication fence — a same-origin fetch from the page is already authorised;
  • registers the conversation_usage agent tool on the same corpus, so the panel and the agent read identical numbers.

Failure isolation: one unreadable/corrupt session becomes an entry in errors and a warning row in the panel; the rest of the corpus still renders.

Browser half (lib/client.js):

  • two slot registrations under one id (usage-stats): sidebar.panellist (the rail icon) and main (the central panel the sidebar addresses by that id);
  • range control, auto refresh, inherited-prefix switch, token trend, four-way runtime split, tool ranking, turn health, sortable/searchable per-conversation table; clicking a conversation opens it through ctx.get('uiWorkspace').openSession;
  • plain JS, no bundler; only react is required — no @deepseek-ai/dsh-client-* module import, which would blank the slot on drift;
  • styles are inline and use only --dsw-alias-* theme tokens;
  • all visible text goes through the Client locale service (zh + en);
  • if the running Host half still serves the v1 payload (it needs an app restart to load new Host code), the panel renders the v1 cards/table with a restart notice instead of crashing on missing v2 fields.

Loading and performance

The panel folds every durable session log, so the first scan is the expensive part (measured here over 88 persisted logs: ~2.1 s CPU to read/fold, then ~12 ms to aggregate the report). Several layers keep that latency out of the way:

  • Warm-up scan: ~1.5 s after the Host half activates it scans the corpus in the background, so the first panel open normally hits a warm cache.
  • Short-lived corpus cache (30 s, DSH_USAGE_STATS_TTL_MS) plus a revision-aware per-session cache: a repeat scan re-reads only sessions whose sessionPersistence revision changed; range/inherited switches re-assemble from memory.
  • Parallel reads across session logs (DSH_USAGE_STATS_CONCURRENCY, default 8).
  • Non-blocking price refresh: when the official price table TTL expires, the report is served from the stale cache/bundled snapshot immediately and the docs page is re-fetched in the background for the next report (pricing.source.pendingRefresh is true meanwhile). The explicit Refresh button still waits for the fresh fetch.
  • Loading UI: a skeleton with a first-load hint (initial load), a slim updating bar plus the existing content during refreshes, and a footer line with the last scan time (timings.scanMs) and cache status. The progress row is always mounted at a fixed height and the Refresh button keeps a fixed width, so switching ranges or refreshing does not shift the panel.
  • DSH_USAGE_STATS_WARMUP_MS=0 disables the warm-up; 0 TTL always rescans.

Route contract

GET /api/usage-stats.summary
    ?range=all|30d|7d|1d        (default all)
    &inherited=1                (include a fork's inherited prefix; default off)
    &offset=<minutes east UTC>  (local day/hour buckets; default 0)
    &force=1                    (bypass the corpus TTL cache and refresh the price table)
→ 200 { ok, version: 5, cached, cacheAgeMs, ttlMs, generatedAt, scanMs,
        timings: { scanMs, summarizeMs, pricingMs, cached, cacheAgeMs },
        range, includeInherited, offsetMinutes,
        scope: { listed, analyzed, failed, live, subagents },
        pricing: { configured, currency, budgetMonthly,
                   source: { kind: 'auto'|'cache'|'bundled'|'mixed'|'manual',
                             url, fetchedAt, ttlHours, provider, userFile, cacheFile },
                   services: { search: { pricePerThousand, billed },
                               fetch: { pricePerThousand, billed } },
                   peakWindows, weekdays, warnings: [], error },
        costing: { configured, currency, budgetMonthly, monthTokens, monthCost,
                   budgetRatio, unpricedModels: [ 'provider/model', … ],
                   serviceCost, servicesConfigured },
        totals: { sessions, liveSessions, subagentSessions,
                  runTimeMs, modelMs, toolMs, humanWaitMs, otherMs,
                  turns, steps, toolCalls, retries, userMessages,
                  assistantMessages,
                  tokens { input, output, cacheRead, cacheWrite, prompt, total },
                  cacheHitRate, cost, monthTokens, monthCost,
                  services: { search, fetch }, serviceCost,
                  inherited { tokens, runTimeMs, turns, steps },
                  firstActivityAt, lastActivityAt },
        sessions: [ { id, title, createdAt, lastActiveAt, live, parentSession,
                      origin, delegationDepth, isSeeded, cwd, workspace,
                      agentPreset, eventCount, runTimeMs, modelMs, toolMs,
                      humanWaitMs, otherMs, turns, steps, toolCalls, retries,
                      userMessages, assistantMessages, tokens, cacheHitRate,
                      cost, unpriced, services, serviceCost, models, tools,
                      turnEnds, inherited } ],
        trend: { unit: 'day'|'hour', buckets: [ { start, tokens, runTimeMs,
                  steps, toolCalls, cost, sessions } ] },
        tools: [ { name, calls, ms, errors } ],
        models: [ { provider, model, calls, tokens, cacheHitRate, cost } ],
        services: [ { kind, calls, billed, pricePerThousand, cost } ],
        turnEnds: { completed, aborted, interrupted, error, maxTokens,
                    blocked, forked, other, open },
        errors: [ { sessionId, message } ], partial }

Per-session `tools` are ranked by execution time; `models` carry the same
token/cost shape as the global list. Unpriced routes keep `cost: null`.
→ 405 non-GET · 503 { ok: false, error }     (e.g. no sessionQuery service)

Agent tool

conversation_usage takes { top?: 1–20 (default 5), range?: 'all'|'30d'|'7d'|'1d', inherited?: boolean, refresh?: boolean } and returns the totals (with the runtime split, per-model tokens, turn health and the estimated cost / month-to-date budget), the highest-consumption conversations and the costliest tools, with the omitted count made explicit. Read-only.

Install

plugin_manager({ action: "install_bundle", target: "@echovic/dsh-usage-stats@0.2.0" })
// or, with the desktop app fully quit:
// dsh plugin --profile desktop add @echovic/dsh-usage-stats@0.2.0

The browser half hot-reloads on save. A change to the Host half (lib/index.js, lib/aggregate.js) needs one application restart, because Host modules are not hot-replaced; until then the panel shows the restart notice and the v1 layout. The same restart is what activates a newly installed Host version.

Test

cd <checkout>
npm test
  • test/aggregate.test.mjs (22) — the folds against synthetic event logs: the four-way split (including parallel tools and approval-inside-tool), token replacement/retry semantics, per-route token attribution, peak/off-peak token splitting, tiered cost, wildcard resolution, range-independent month-to-date cost, turn endings, open turns, range clipping, local day/hour trend buckets, per-session tool/turn detail, Search/Fetch request counting and per-1,000 service billing, and the inherited-prefix mode;
  • test/pricing.test.mjs (9) — official-page parsing (CNY and USD fixtures), user-entry normalization, the peak-hour calendar, and the resolution ladder Search/Fetch service-price parsing and overrides, stale-while-revalidate background refresh, and the resolution ladder (live fetch → cache → bundled snapshot → manual override → malformed file);
  • test/route.test.mjs (16) — apply() registration, the Fetch handler, range/inherited parameters, manual and auto-fetched pricing (including a mocked official page and the offline fallback), Search/Fetch billing, the corpus TTL cache, revision-aware per-session reuse, failure isolation and the agent tool;
  • test/client.test.mjs (12) — boots the real browser module in a vm with a stateful React double, renders the full v4 dashboard in zh and en, opens the detail drawer by clicking a conversation row, renders the Search & Fetch section, checks the v4/v3/v2-payload compatibility notices, and renders the legacy v1 payload with its restart notice, asserting both slot registrations, complete locale dictionaries and theme-token-only colors.

Known limits

  • Token totals only count usage the provider reported in the log; sessions from providers that omit usage contribute zeros.
  • Cost is an estimate, not a bill: it follows the official docs page (or your overrides), so discounts, private pricing, or a docs change between fetches can differ from the invoice. Unpriced models are excluded from money figures (and reported as unpriced); the month figure is local-calendar based.
  • Peak/off-peak classification knows weekdays only — Chinese statutory holidays are billed as off-peak even though the official page excludes them from peak.
  • Search cost counts Search API requests belonging to a successful web_search call; a call that fails late can under-count requests it already issued, since per-request success is not recorded. Requests without a matching call are not billed.
  • web_fetch runs through the local HTTP provider and is unbilled by default; opt into Fetch billing via services.fetch.billed only for a billable Fetch provider.
  • When both the live page and a fresh cache are unavailable, the bundled official-prices.json snapshot is used until a refresh succeeds.
  • Runtime is turn wall time — it includes human waits and queueing; the split separates the human part explicitly.
  • tools[].ms is summed per-call execution time (workload), so parallel tool calls may sum past the wall-clock tool segment.
  • The panel is a snapshot; a live conversation's last flush may trail by up to the cache window unless Refresh is pressed.
  • Local day/hour buckets need the browser's timezone offset; a page that cannot report one gets UTC buckets.

Published package

  • repository: https://github.com/echoVic/dsh-usage-stats (tags: v0.2.0)
  • npm: https://www.npmjs.com/package/@echovic/dsh-usage-stats
  • install from npm: plugin_manager({ action: "install_bundle", target: "@echovic/dsh-usage-stats@0.2.0" })
  • install from git: plugin_manager({ action: "install_bundle", target: "github:owner/repo#v0.2.0" }) (any https git URL works too; the repository root must be this package)
  • the bundle ships the Host half, the Web client half, the Loader patch, both locale files and the icon; there are no runtime dependencies and no build step.
  • the Host half runs with the application's local permissions, so read a third-party bundle before installing it.

License

MIT © 2026 echovic

About

DeepSeek Harness conversation usage statistics plugin: runtime split, token trend, tool ranking, turn health, agent tool

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages