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
@@ -145,6 +145,7 @@ mod tests {
|
||||
output: format!("{}<{}>", req.role, req.context.join("|")),
|
||||
tokens: 10,
|
||||
gated: vec![],
|
||||
spend: Default::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,6 +147,7 @@ mod tests {
|
||||
output: format!("{}:{}", req.role, req.context.join(" ")),
|
||||
tokens: 10,
|
||||
gated,
|
||||
spend: Default::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,15 +95,35 @@ pub struct TurnRequest {
|
||||
pub context: Vec<String>,
|
||||
}
|
||||
|
||||
/// What a turn cost and who was paid — the part of the runtime's `done` frame
|
||||
/// that `tokens` alone threw away.
|
||||
///
|
||||
/// `tokens` stayed as the one total every reader already keys on. This is
|
||||
/// the split beside it, plus the provider and model that answered, so the
|
||||
/// spend can be asked per provider BEFORE a plan limit asks it for you. Judge
|
||||
/// spend gained this on 2026-09-14 and agent spend did not, which left the
|
||||
/// larger of the two invisible.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Spend {
|
||||
pub input_tokens: u64,
|
||||
pub output_tokens: u64,
|
||||
/// Provider family that answered (`anthropic`, `glm`, …), when the
|
||||
/// runtime said. `None` on executors that do not report one.
|
||||
pub provider: Option<String>,
|
||||
pub model: Option<String>,
|
||||
}
|
||||
|
||||
/// The result of a single agent turn.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TurnOutcome {
|
||||
/// The turn's textual output.
|
||||
pub output: String,
|
||||
/// Model tokens spent (cost proxy).
|
||||
/// Model tokens spent (cost proxy). Input + output.
|
||||
pub tokens: u64,
|
||||
/// Any sandbox-leaving actions attempted during the turn.
|
||||
pub gated: Vec<GatedAction>,
|
||||
/// The split and the provider behind `tokens`.
|
||||
pub spend: Spend,
|
||||
}
|
||||
|
||||
/// Runs one safe agent turn. The real impl wraps `cm-runtime::Runtime`
|
||||
@@ -143,6 +163,10 @@ pub struct StepRecord {
|
||||
pub gated: Vec<GatedAction>,
|
||||
/// Tokens it spent.
|
||||
pub tokens: u64,
|
||||
/// The split and provider behind `tokens`. `default` so checkpoints
|
||||
/// journaled before this field existed still load.
|
||||
#[serde(default)]
|
||||
pub spend: Spend,
|
||||
}
|
||||
|
||||
/// The full record of a topology run (journal + final output + totals).
|
||||
@@ -264,6 +288,7 @@ where
|
||||
output: outcome.output,
|
||||
gated: outcome.gated,
|
||||
tokens: outcome.tokens,
|
||||
spend: outcome.spend,
|
||||
});
|
||||
|
||||
// Hand the caller a durable snapshot to persist before the next turn.
|
||||
@@ -309,6 +334,7 @@ mod tests {
|
||||
output: format!("{}({})<{}>", req.role, req.node_id, req.context.join("|")),
|
||||
tokens: 10,
|
||||
gated,
|
||||
spend: Default::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -383,6 +409,7 @@ mod tests {
|
||||
output: format!("{}({})<{}>", req.role, req.node_id, req.context.join("|")),
|
||||
tokens: 10,
|
||||
gated: vec![],
|
||||
spend: Default::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,7 @@ impl TurnExecutor for ProviderExecutor {
|
||||
tokens,
|
||||
// Tool-free reasoning turns leave the sandbox nowhere.
|
||||
gated: vec![],
|
||||
spend: Default::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ mod tests {
|
||||
output: format!("{}<{}>", req.node_id, req.context.join("|")),
|
||||
tokens: 5,
|
||||
gated: vec![],
|
||||
spend: Default::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user