﻿# Record per-response token usage as a conversation event

- **Status**: Todo
- **Kind**: Feature
- **Authors**: jp
- **Date**: 2026-09-18
- **Label**: domain=conversation
- **Label**: domain=llm
- **Label**: package=jp_conversation
- **Label**: package=jp_llm
- **Label**: type=feature

Providers report token counts on every response and some report cost.
JP carries none of it into anything durable.
`jp_openrouter::responses::Usage` and `jp_openrouter::types::response::Usage`
exist as wire types with `input_tokens`, `output_tokens` and `cost`; nothing
lifts them into `jp_llm::Event` or the conversation stream.
No other provider's usage is read at all.

## What it costs

JP cannot answer "did that change help?" for any change to the harness.
Every other proposal in T-0n0nwz3 is unfalsifiable without this: there is no way
to tune a compaction threshold, judge whether a turn budget fires too early, or
tell whether moving a plan out of history saved anything.

The harness study's whole contribution is that it measured.
It reports cost per task, peak context as a fraction of the window, turns, and
tool calls per task, and every one of its findings is a comparison between those
numbers.
JP has the richer substrate, a durable event stream with stable event IDs (RFD
097), and none of the measurements.

Day to day, a user also has no way to see what a conversation has cost.

## Shape

A `Usage` event kind on the conversation stream, appended per provider response.
As an event it inherits durability, provider-invisibility under projection, and
the whole `jp conversation` read surface without new machinery.

Fields worth carrying: input, output, cache-read and cache-write token counts,
the resolved model ID, and cost where the provider reports it.

Where the pieces belong:

- Normalization in `jp_llm::Event`, because providers disagree on shape and the
  disagreement should not leak past the provider boundary.
- The event type in `jp_conversation::event`.
- Recording in the turn loop, alongside the existing mid-turn flush.

T-0fedmkq needs the same plumbing from the other end: it wants the served
service tier recorded alongside the turn, and notes that the OpenRouter provider
never sets the request's `usage` flag, so `Usage.cost` arrives unpopulated.
Worth doing together.

## Caveat

This is a signal, not a target.
Goodhart applies the moment a number like "tokens per turn" becomes something to
optimize directly.

Findings and the rest of the proposals: T-0n0nwz3.
