deepseek-harness/.agents/notes/implemented/feature/2026-08-24-web-per-turn-token-usage.md
Ziya b565df3442 feat(web): show exact per-turn token usage (#3005)
* feat(web): show exact per-turn token usage

* test(runtime): refresh exact token usage snapshots

* refactor(token-meter): own per-turn usage folding

* perf(ui-chat): bound paging anchor layout reads

* test(web): align usage golden with system prompt row

* fix(test): resolve token-meter client from source

* test(token-meter): cover retry without usage

---------

Co-authored-by: ZiyaZhang <199893125+ZiyaZhang@users.noreply.github.com>
2026-08-25 19:05:52 +08:00

3.4 KiB

Agent Note: Exact Web per-Turn token usage

Status: implemented

English | 中文

Problem

Web Chat exposes cumulative session token usage near the composer, but that value cannot explain the cost of one completed Turn. A paged history window may begin inside a Turn, retries may consume several model calls, streaming and final events may repeat one attempt's usage, and optional cache fields do not prove an exact total. Displaying a partial subtotal as Turn usage would make recorded provider facts look more complete than they are.

Decision

The shared TokenUsage value carries optional totalTokens for one model call. Adapters publish it only from an exact provider total or authoritative aggregate prompt and output counters. DeepSeek checks its prompt-plus-completion aggregate against any wire total, and pi-ai preserves its provided total.

Token-meter owns a browser-safe pure Turn-local fold over durable session events, shared with its retry-aware cumulative usage projection. step/start and llm/retry-started open actual attempts; a final assistant message replaces the same attempt's streaming sample; terminal failures, retries, and step boundaries close attempts without double counting. Every started attempt must close with safe non-negative integer usage and an exact total. Optional cache, reasoning, and route aggregates appear only when every contributing attempt reports them, and reasoning remains a subset of output.

Web Chat selects a Turn only when its loaded match window includes turn/start, passes that complete durable-event window to the token-meter fold, and renders the result. A complete, exact result appears through a local-state DisclosureRow above the existing actions; incomplete or contradictory evidence produces no row. Chat owns no token-accounting state machine.

Alternatives considered

Subtract neighboring cumulative session values. Rejected because pagination, compaction, retry coverage, and projection completeness can make adjacent values incomparable; subtraction would infer data that no call reported.

Publish historical per-Turn values through a new client session projection. Rejected because the loaded per-Turn view already has the durable attempt events it needs, while a history-growing projection would add transport, persistence, and versioning costs. Reusing token-meter's pure fold keeps one accounting owner without adding another wire value.

Show known buckets without an exact total. Rejected because a lower-bound subtotal presented in a completed Turn footer is indistinguishable from a complete bill.

Consequences

New provider records can expose exact per-Turn accounting without a new transport or persisted UI state. Older sessions and adapters without enough evidence simply omit the disclosure. Model routes disappear as a group when any billed attempt lacks attribution, while trustworthy token totals remain visible.

Focused adapter, token-meter fold/projection, component, pagination, and assembled Web replay tests pin total preservation, retry-attempt separation, fail-closed validation, optional-field omission, interaction, and full-window publication. The cumulative projection and exact Turn fold now share token-meter ownership; the projection remains a whole-log bucket view, while the fold alone makes the stricter exactness and completeness claim required by the disclosure.