feat(billing): agent-side spend records who was paid

Judge spend gained provider, model and mission on 2026-09-14; agent spend —
the larger half — did not. The runtime's `done` frame has always carried
`model` and `provider` beside the two token counts, and `topology_exec` read
only the counts, summed them, and charged the sum as output with no record of
which provider served the turn.

`TurnOutcome` and `StepRecord` carry a `Spend` now (input/output split,
provider, model), the worker passes it through `cm_billing::charge` along with
the mission id, and the chat runtime records the model it requested — that
loop drives one provider with no chain, so requested is answered. A bare
model name is recorded without a guessed family. `StepRecord.spend` is
`serde(default)` so journaled checkpoints from before this field still load,
and `tokens` stays as the total every reader keys on.

`charge` moved from `query!` to `query`: the macro pins the statement to
offline metadata that a schema change then has to regenerate against a live
database, for columns that are nullable text and uuid.

The done-frame test now asserts the split and the provider survive, not just
the sum.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZb5A2kfVfjpdwSochkuHz
This commit is contained in:
Omar Sobh
2026-09-14 08:17:20 -05:00
co-authored by Claude Opus 5
parent 736b6a9a82
commit 483de9f88a
13 changed files with 128 additions and 18 deletions
+2 -2
View File
@@ -62,7 +62,7 @@ async fn charges_span_lots_oldest_first_and_record_usage() {
.unwrap();
// 2500 tokens → 3 credits: drains the first lot (2) then one more.
let deducted = charge(&pool, ws.id, agent.id, Some(run_id), 1500, 1000)
let deducted = charge(&pool, ws.id, agent.id, Some(run_id), 1500, 1000, None, None, None)
.await
.unwrap();
assert_eq!(deducted, 3);
@@ -96,7 +96,7 @@ async fn an_empty_workspace_records_usage_but_clamps_at_zero() {
.unwrap();
// Owes 5, only 1 available: deducts 1, balance hits zero, never negative.
let deducted = charge(&pool, ws.id, agent.id, Some(run_id), 4000, 500)
let deducted = charge(&pool, ws.id, agent.id, Some(run_id), 4000, 500, None, None, None)
.await
.unwrap();
assert_eq!(deducted, 1);