Hovering an assistant message reveals a quiet monospace chip (same
fade/slide choreography as the copy link) showing tokens/sec, cost
(~-prefixed when estimated), and total tokens. Hovering the chip opens
a sectioned stats card: input tokens with cache read/write sub-rows and
cache-hit rate, output, total, time to first token, total time, speed,
and cost with an estimated/reported qualifier. Usage arrives on the
message_usage goose notification and is attached to the matching
message (falling back to the last assistant message) in the ACP
adapter. Also adds the missing 'group' class on the tool-call footer
wrapper, which the timestamp's group-hover classes already assumed.
New sessionUpdate variant on _goose/unstable/session/update carrying
{messageId?, usage} keyed by the id already used for chunk matching.
Sent live from the prompt stream loop and on session replay after each
message's content chunks, both gated on the client's goose custom
notification capability. Regenerates acp-schema.json and the SDK types.
attach_turn_usage runs after every AgentEvent::Message of the turn is
yielded (usage arrives on a trailing usage-only provider chunk), so
live consumers could never see per-message usage. The new
AgentEvent::MessageUsage carries the target message id and the attached
MessageUsage; non-desktop consumers ignore it.
Only the local MLX provider reported elapsed_ms and nothing reported
time_to_first_token_ms, so per-message timing would be empty for every
hosted provider. The stream wrapper now captures request start before
the provider call and first-content time in both the toolshim and
passthrough branches, filling usage.stats only when the provider left
the fields unset.
Source TokenState and the ACP usage updates from the on-the-fly aggregation
(session + sub-agent tree) rather than the stored session record, so clients
see the derived per-message totals including delegated sub-agent spend.
Resolve each turn's best-estimate cost (provider-reported when present, else
catalog) once in update_session_metrics and record it to the ledger atomically
with the session totals - for normal turns, auto-compaction, and /compact.
Link sub-agent sessions to their parent so their usage rolls into the parent's
grand total. Attach the turn's usage to its assistant message for display.
Add an append-only usage_ledger table (migration v15) and a persisted
sessions.parent_session_id link. The ledger is decoupled from the messages
table so per-call usage survives conversation rewrites (compaction) and
cancelled turns, and each row records the model that produced it so estimated
costs can be recomputed or broken down per model later.
Session totals are derived by summing the ledger across a session and its
sub-agent descendants (recursive CTE over parent_session_id), with a
per-session fallback to the retained accumulated_* record for sessions that
predate the migration.
record_usage_metrics runs in one atomic transaction: it first reconciles the
ledger with any spend recorded outside it (pre-v15 history, or turns run on a
pre-v15 build after switching branches) via a 'carried_forward' adjustment row
- both builds only ever grow accumulated_*, so the componentwise difference is
exactly the spend the ledger missed - then adds the call's delta to the
accumulated columns in SQL (so concurrent writers can't lose updates) and
appends the ledger row. The derived total and the legacy fallback therefore
converge again after any branch round-trip.
Add MessageUsage (tokens, cost, cost_source, generation timing) on
MessageMetadata, populated and serialized to clients as groundwork for the
per-message UI, and register it plus CostSource in the OpenAPI schema.
Add optional cost + cost_source (ProviderReported/Estimated) to ProviderUsage
and parse `usage.cost` from OpenRouter and Anthropic-compatible gateways across
the streaming and non-streaming OpenAI and Anthropic paths. Request OpenRouter's
inline cost via `usage: {include: true}`. When no provider cost is present the
cost is filled in downstream from the catalog estimate.
Drop the unused ProviderUsage::combine_with rather than extending it with cost
semantics nothing exercises.