Skip to content

feat: per-message usage/cost tracking with derived session totals - #10172

Merged
DOsinga merged 7 commits into
mainfrom
feat/per-message-usage
Jul 5, 2026
Merged

feat: per-message usage/cost tracking with derived session totals#10172
DOsinga merged 7 commits into
mainfrom
feat/per-message-usage

Conversation

@filipkujawa

@filipkujawa filipkujawa commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

Records token usage and cost per provider call and derives session totals by aggregation, replacing both the independently accumulated session counter and the sub-agent roll-up approach from #10103.
This follows the direction from @DOsinga's review on #9719: every chunk carries its best cost estimate (provider-reported when available, catalog-derived otherwise), filled in once, and the session total is derived from the per-chunk records so it cannot drift.

What changed

  • ProviderUsage gains cost / cost_source, parsed from usage.cost across the OpenAI and Anthropic-compatible paths, with OpenRouter asked to return cost inline via usage: {include: true}.
  • Each chunk's cost is resolved exactly once in update_session_metrics (provider-reported when present, else catalog estimate), the single point all four usage producers share.
    The catalog estimate needs a provider name and the Provider trait has no single wrapper to decorate, so this shared waist is the only place "filled exactly once" is actually enforceable.
  • New append-only usage_ledger table (schema v15) records one row per provider call, kept separate from messages so spend survives compaction rewrites and message-less turns.
  • sessions.parent_session_id is persisted for sub-agent sessions, and totals are derived with a recursive CTE so a parent reports the grand total across its tree while each sub-agent stays a distinct record.
  • MessageUsage on MessageMetadata serializes per-message usage to clients as display-only groundwork for the hover UI (follow-up PR).
  • TokenState and the ACP usage updates are sourced from the derived totals.

Migration

v15 is purely additive and accumulated_* stays dual-written in the same transaction as the ledger row, so a v15 database still works on main and the two records can't diverge.
Spend the ledger missed (pre-v15 history, or turns run on main) is appended as a carried_forward row on the next write, so totals survive branch round-trips.

Scope decisions

  • Only summon-delegated sub-agents are linked to their parent.
    Orchestrator-spawned sessions are created as SessionType::User peers and intentionally stay unlinked.
  • The CLI session summary, session list, and insights keep reading the per-session stored columns.
    List rows stay per-session by design: summing each row's tree would double count sub-agents that are also rows.
  • Calls that discard usage today (session naming, permission judging, tool-pair summarization) remain untracked, preserving parity with main.
  • If OpenRouter's streamed cost is absent we fall back to the estimate; the /api/v1/generation follow-up call is not implemented.

Follow-ups

  • PR B: desktop per-message hover UI showing per-message tokens, cost with an estimated/reported badge, and tokens/sec from elapsedMs.
  • Once v15 is deployed everywhere, the accumulated_* dual-write and reconciliation can be removed, leaving totals purely ledger-derived.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 42e834cafe

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread crates/goose/src/session/session_manager.rs Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 84bb651d9e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread crates/goose/src/session/session_manager.rs Outdated
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.
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 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.
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.
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.
@filipkujawa
filipkujawa force-pushed the feat/per-message-usage branch from 84bb651 to 8c8dfa4 Compare July 2, 2026 16:06
@filipkujawa
filipkujawa marked this pull request as draft July 2, 2026 16:09
@filipkujawa
filipkujawa marked this pull request as ready for review July 2, 2026 19:15

@DOsinga DOsinga left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. Codex feedback has been addressed, CI is green, and I pushed a small cleanup commit to strip redundant comments before approving.

@DOsinga
DOsinga merged commit 9b837f1 into main Jul 5, 2026
24 of 25 checks passed
@DOsinga
DOsinga deleted the feat/per-message-usage branch July 5, 2026 20:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants