Repository navigation
feat(usage): separate auxiliary model calls from a Session's cost and cache hit rate #5691
Description
Problem
A Session's usage figures in Desktop mix the main agent loop with every auxiliary model call made for that Session, and there is no way to separate them.
- Auxiliary calls pass
telemetrySessionId: sessionIdtorunHostAuxiliaryModelCall. These are Session title, recap, goal evaluation, memory extraction, WorkHub intent/recall, and next-prompt suggestions once feat(desktop): add opt-in next prompt suggestions #5629 lands. - The Session Inspector loads
usage:summarywith{ range: 'all', sessionId }(apps/desktop/src/preload/preload.ts:1367). That summary feeds bothestimatedSessionCostandusageCacheHitRate(session-inspector-overview-model.ts:194,:245-254, computed ascacheRead / input). UsageQuery(packages/core/src/usage-stats/types.ts:42-50) can filter by session, connection, provider, model, tool and status, but not bycallKind. Consumers therefore cannot exclude auxiliary calls, and cannot break a Session's cost down by kind.
Until now this mattered little, because most auxiliary calls run once or rarely per Session. A per-reply auxiliary call such as #5629 changes that:
- Cost. Every reply adds an extra call that the user sees as Session cost, with no way to tell it apart from the agent's work.
- Cache hit rate. Auxiliary prompts do not share the main loop's cached prefix. Example: a main turn of 100k input tokens at 95% cache reads, plus one uncached 10k-token suggestion call, moves the displayed rate from 95% to about 86%. The main loop's caching did not change at all.
- Daily Review. Its request counts are inflated in the same way.
A second, smaller accuracy gap is on the failure path of runHostAuxiliaryModelCall (execution-model-authority.ts:691-701). When a call is aborted or fails, it is recorded with inputTokens: 0, outputTokens: 0, even though the provider may already have processed, and billed, the request. For example, a reasoning call aborted by a short deadline could have been billed for its input plus any reasoning tokens.
Desired outcome
- Add a
callKind(orcallKinds/excludeCallKinds) filter toUsageQuery, so consumers can ask for the main loop only, or for a per-kind breakdown. - Base the Session Inspector's cache hit rate on
maincalls only. Optionally show auxiliary spend as its own line next to the Session cost, instead of folding it in silently. - Where the provider or SDK reports partial usage on abort or error, record it. If it can't be known, mark the row as usage-unknown rather than zero.
Alternatives or workarounds
- Auxiliary calls could omit
telemetrySessionId, so they only appear in global usage. That hides them from the Session entirely, which is worse for cost transparency. - feat(desktop): add opt-in next prompt suggestions #5629 could state in its copy that suggestion spend is counted in the Session's cost. That documents the mixing but does not fix the hit-rate distortion.
Found while reviewing #5629 (review: #5629 (review)).
AI disclosure: Claude Code (Claude Opus) traced the usage path at 276faf1c9 and drafted this issue; liugddx reviewed it and is the human contributor of record.
Activity
ggbdpq commentedon Sep 24, 2026
take
anbv29 commentedon Sep 24, 2026
Hi! I’d like to verify which documentation snapshot is authoritative, reproduce the version/links problem, and propose a focused correction. Is there a preferred owning repository for the fix?
fix(desktop): hide the main cache rate when the main-only summary rea…
fix(desktop): hide the main cache rate when the main-only summary rea…
feat(usage): filter usage queries by model call kind
fix(desktop): hide the main cache rate when the main-only summary rea…
feat(usage): filter usage queries by model call kind
fix(desktop): hide the main cache rate when the main-only summary rea…
fix(desktop,storage): degrade a crashed main-only usage read; documen…
ggbdpq commentedon Sep 30, 2026
Picking up part 3 of this issue (the abort-path accuracy gap) — we hold the issue. Design intent before code, per the scoping we did internally:
Scope (minimal core).
runHostAuxiliaryModelCall's failure catch (~745-757 inexecution-model-authority.tsat9b089f58e) writesinputTokens: 0, outputTokens: 0for aborted/failed calls. Per the issue's desired outcome, we'll record these rows as usage-unknown rather than zero, reusing the existing canonical machinery:ModelCallUsageBasis('reported'|'partial'|'missing'), the schema CHECK that forbids token counts on missing rows, and the read-side coverage sums. The SDK reports no partial usage on abort (verified intool-free-model-call.ts), so "record partial" has no room on this path — unknown is the honest value.One design decision to flag: auxiliary calls happen outside an agent run, so the canonical writer (
run.recordModelCallAttempt) is unreachable; the authority's only write seam today is the legacyusage_llm_callstable. We plan to add a write method toInteractiveUsageStoresWriter.modelCalls(mirroring the existingserver/canonical-usage-reader.tsread-seam precedent) and migrate both record paths (failure + zero-usage success) onto it. If maintainers would rather see a different seam shape, say so and we'll adapt — the migration surface is small (~150-250 lines, 4-6 production files).Out of scope for this round: exposing unknown-usage counts over the wire to Desktop (that touches
protocol/index.tsand the overview payload — happy to add after this lands if wanted), and thecreateHostPluginModelcallKind: 'main'routing (we'll document it in the PR rather than change it here).Generated-by: GLM-5.3-Flash (ZCode)
fix(usage): keep zero-usage auxiliary completions on the legacy path
fix(usage): settle hosted executions despite auxiliary usage-unknown …
fix(usage): give hosted settlement its own run-scoped incompleteness …
fix(usage): carry no-run ownership in a column, not the turn value
fix(usage): drop the no_run backfill that reused the collision-prone …
feat(usage): filter usage queries by model call kind (#5709)
fix(usage): record aborted auxiliary model calls as usage-unknown, no…
liugddx commentedon Oct 8, 2026
This closed automatically when #5709 merged, before #5890 had landed. Both have now landed, so it is complete:
callKindsfilter onUsageQuery: feat(usage): filter usage queries by model call kind #5709 (epoch 208). Without it, queries behave as before; unknown kinds are rejected at the protocol decoder.maincalls only: the Inspector's summary now also reads the Session scoped tocallKinds: ['main'](apps/desktop/src/preload/usage-summary.ts:50), so auxiliary prompts no longer dilute the rate.no_runrows, while still checking delegated child Sessions in its window.Thanks @ggbdpq for carrying both PRs through many review rounds.
Not done, and not blocking this close: