@deepseek-ai/dsh-token-meter provides ctx.tokenMeter, a single replay-aware measurement service that answers two questions from the durable session log: what will the next request cost (request pressure) and how much model-visible content is live right now (surface). It never needs live adapter state — everything is derived from session events, so it is deterministic, restart-proof, and projection-friendly.
Package
| Package | Description |
|---|---|
@deepseek-ai/dsh-token-meter | "Replay-aware token measurement service (ctx.tokenMeter)" |
It peers with dsh-llm, dsh-session, dsh-compaction, dsh-session-projection, dsh-invariants, and cordis; the only non-workspace dependency is zod@^4.4.3.
What is measured
The service measures tokens, not money. "Usage accounting" in this harness is split across two complementary views:
- Provider-reported usage — the exact
usagechunk from each model call, stored on the sessionassistant/messageevent and folded into thetokenUsageprojection (these are real token counts the provider returned). - Heuristic pressure/surface —
TokenMeter.measure(session, requestHeader?)replays the log and prices a "surface" (the ordered set of model-visible messages) plus a "baseline" anchor. This is a prediction of the next request's cost, used for compaction and long-context decisions.
There is no per-token price table or dollar cost estimate anywhere in the meter — billing/cost is out of scope for the seam.
The data model
// packages/llm/token-meter/src/types.ts
export type TokenMeasurementBaseline =
| { readonly kind: 'none'; tokens: 0 }
| { readonly kind: 'estimated'; tokens: number } // full heuristic price
| { readonly kind: 'usage'; tokens: number; usage: Readonly<TokenUsage> } // provider-anchored
export interface TokenMeasurement {
logRevision: number // = next unread event seq
baseline: TokenMeasurementBaseline
surfaceDeltaTokens: number // signed repricing vs. the baseline anchor
totalTokens: number // non-negative current request+response pressure
surfaceTokens: number // heuristic total across the current surface
nodes: readonly TokenSurfaceNode[] // ordered (seq, tokens) per surface position
}Baselines
measure() picks a baseline for the request header (the canonical request envelope, i.e. provider/model/system/tools):
usage— when the latest successful call's canonical envelope matches the provided header and its provider total is at least the full heuristic price of that call (conservative-anchor rule). The provider usage is reused as-is, and only the signed surface delta since that call is repriced.estimated— otherwise, the envelope (system prompt + tool schemas) and the current surface are heuristically repriced.none— before any request with an empty surface.
This avoids paying a heuristic on every read while keeping correctness: a big new message since the anchor is added; a compaction that shrank the surface is subtracted.
The fixed heuristic estimator
packages/llm/token-meter/src/estimate.ts is a deliberately simple, fixed-density estimator (shared verbatim with the contextBreakdown projection so both surfaces agree):
| Element | Price |
|---|---|
| density | 4 characters per token (CHARS_PER_TOKEN) |
| structural overhead per block | 4 tokens (BLOCK_OVERHEAD) |
| role-field framing per message | 4 tokens (ROLE_OVERHEAD) |
| system prompt | ceil(system.length / 4) + ROLE_OVERHEAD |
| tool schemas | ceil(JSON.stringify(tools).length / 4) + BLOCK_OVERHEAD |
| unknown block | conservative structural JSON price |
estimateMessage, estimateHeader, estimateContent are exported pure functions, so callers can price a single message without a session.
Events and how the loop feeds it
There is no dedicated "usage event". The meter eavesdrops on ordinary session events:
| Event | Role in measurement |
|---|---|
request/header | establishes the canonical request envelope (system, tools, config) |
step/start / step/end | delimit one model step (validates assistant events belong to an open step) |
assistant/chunk | raw stream chunks; a usage chunk provides an early provider sample |
assistant/message | finalizes the step; carries the definitive usage (the meter's primary anchor) |
surface events (user/message, assistant/message, …) | fold into the token-priced surface |
The agent loop (packages/core/agent-loop/src/agent.ts) streams chunks via ctx.llm, appending each as assistant/chunk, then appends assistant/message (with usage and sourceEventSeqs) at step end — which is exactly what the meter replays. BlockAssembler is reused by the meter to reassemble provider output from cited chunk seqs so it can compare provider usage against the heuristic price without a second stream.
Projections (where usage surfaces)
Through the optional ctx.sessionProjections registry, the meter registers three pure replay projections:
| Projection | Key | View output |
|---|---|---|
| Token usage | tokenUsage | { uncachedInputTokens, outputTokens, cacheReadTokens, cacheWriteTokens } totals, deduped per turn/step |
| Context pressure | contextPressure | { contextWindow?, pressureTokens?, projectedTokens? } — prompt-side occupancy from the newest usage sample plus surface movement |
| Context breakdown | contextBreakdown | per-source pricing of the current surface (instructions/catalog/snapshot/… rows) |
tokenUsage is what the UI reads for cumulative model usage, and contextPressure for context-window occupancy. Both are last-wins and accumulate without double-counting: a repeated usage for the same turn/step replaces the earlier value rather than adding again.
Where usage appears in the UI (brief)
packages/client/ui-settings-modelsand the session header surface usesession.modelsandsessionModelSelectfrom the API gateway, not the meter directly.- The trajectory/session header consumes the
tokenUsageandcontextPressureprojections (delivered assession/projectionframes on the mux event stream and seeded by the history tail's projections block).
The meter itself is host-side; the client sees it through the session-projection pipeline and the forwarded settings/document-updated/llm/adapters-updated invalidation events.
Packages
| Package |
|---|
@deepseek-ai/dsh-token-meter |
@deepseek-ai/dsh-llm (defines TokenUsage) |
@deepseek-ai/dsh-session-projection (registry) |
Further reading
- The LLM layer — the
usageStreamChunk,TokenUsage, and theassistant/chunk/assistant/messageevents the meter replays. - DeepSeek provider — how adapter
mapUsageproduces disjoint counts the meter consumes. - API gateway — how the client receives usage via
session/projectionframes andsession.models. packages/llm/token-meter/src/index.ts—TokenMeter,measure,_sync, the baseline selection.packages/llm/token-meter/src/estimate.ts— the fixed heuristic estimator.packages/llm/token-meter/src/usage-projection.ts— thetokenUsage/contextPressureprojection folds.