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:
co-authored by
Claude Opus 5
parent
736b6a9a82
commit
483de9f88a
@@ -36,21 +36,36 @@ pub async fn charge(
|
||||
run_id: Option<Uuid>,
|
||||
input_tokens: u64,
|
||||
output_tokens: u64,
|
||||
// Who was paid, and for which mission. `None` where the executor did not
|
||||
// say. Until 2026-09-14 no agent-side row carried these, so the larger
|
||||
// half of the spend could not be asked per provider — the judge's half
|
||||
// could, and that is how a plan emptying twice went unexplained.
|
||||
provider: Option<&str>,
|
||||
model: Option<&str>,
|
||||
mission_id: Option<Uuid>,
|
||||
) -> Result<i64, BillingError> {
|
||||
let owed = credits_for_tokens(input_tokens + output_tokens);
|
||||
let mut tx = pool.begin().await?;
|
||||
|
||||
sqlx::query!(
|
||||
// `sqlx::query`, not `query!`: the macro pins this statement to offline
|
||||
// metadata that a schema change then has to regenerate against a live
|
||||
// database, and the columns added by migration 0085 are nullable text
|
||||
// and uuid — nothing here that a compile-time check would catch.
|
||||
sqlx::query(
|
||||
"INSERT INTO usage_events
|
||||
(workspace_id, agent_id, run_id, kind, tokens_in, tokens_out, credits)
|
||||
VALUES ($1, $2, $3, 'llm_tokens', $4, $5, $6)",
|
||||
workspace_id.as_uuid(),
|
||||
agent_id.as_uuid(),
|
||||
run_id,
|
||||
input_tokens as i64,
|
||||
output_tokens as i64,
|
||||
sqlx::types::BigDecimal::from(owed),
|
||||
(workspace_id, agent_id, run_id, kind, tokens_in, tokens_out, credits,
|
||||
provider, model, mission_id)
|
||||
VALUES ($1, $2, $3, 'llm_tokens', $4, $5, $6, $7, $8, $9)",
|
||||
)
|
||||
.bind(workspace_id.as_uuid())
|
||||
.bind(agent_id.as_uuid())
|
||||
.bind(run_id)
|
||||
.bind(input_tokens as i64)
|
||||
.bind(output_tokens as i64)
|
||||
.bind(sqlx::types::BigDecimal::from(owed))
|
||||
.bind(provider)
|
||||
.bind(model)
|
||||
.bind(mission_id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user