harness-core: model pricing + per-step cost accumulation (M3)

- types::ModelCost {input, output, cache_read, cache_write} (USD per 1M tokens)
  with cost_of(usage); reasoning tokens are billed within output by our providers
  so they are not charged separately. ModelInfo gains cost + reasoning/tool_call/
  attachment capability flags (all #[serde(default)] for forward-compat).
- Engine: RunConfig.cost threads pricing into process_step; on_finish now stamps
  the real dollar cost onto the StepFinish part; StepOutcome carries per-step cost.
- run_session accumulates each step's usage and cost onto the session (previously
  never updated) and republishes SessionUpdated — best-effort, store errors logged
  not fatal.
- Store gains a single-session getter (Session cmd + get_session).
- App passes cost: None for now (real rates land with models.dev wiring).
- Tests: ModelCost::cost_of math (+ reasoning exclusion), and the multi-step
  engine test now asserts session usage/cost accumulation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-08 23:45:21 +02:00
co-authored by Claude Opus 4.8
parent 39d62348d8
commit 2f3dfe2305
7 changed files with 151 additions and 11 deletions
+10 -2
View File
@@ -26,6 +26,8 @@ pub struct StepOutcome {
pub result: StepResult,
pub message_id: Option<MessageId>,
pub usage: TokenUsage,
/// Dollar cost of this step's usage (0.0 when no pricing is available).
pub cost: f64,
pub aborted: bool,
}
@@ -513,6 +515,7 @@ impl<'a> Run<'a> {
&mut self,
reason: FinishReason,
usage: TokenUsage,
cost: f64,
) -> Result<StepResult, ProviderError> {
let part = Part {
id: PartId::new(),
@@ -521,7 +524,7 @@ impl<'a> Run<'a> {
idx: self.next_idx,
body: PartBody::StepFinish {
usage,
cost: 0.0,
cost,
reason: reason.clone(),
},
};
@@ -567,10 +570,12 @@ pub async fn process_step(
ctx: &StepContext,
model: crate::types::ModelRef,
agent: &str,
cost: Option<crate::types::ModelCost>,
doomloop: &mut DoomLoopGuard,
) -> Result<StepOutcome, StepError> {
let mut run = Run::new(ctx);
let mut usage = TokenUsage::default();
let mut step_cost = 0.0;
let mut result = StepResult::Stop;
loop {
@@ -582,6 +587,7 @@ pub async fn process_step(
result: StepResult::Stop,
message_id: run.assistant.as_ref().map(|m| m.id.clone()),
usage,
cost: step_cost,
aborted: true,
});
}
@@ -630,7 +636,8 @@ pub async fn process_step(
usage: finish_usage,
} => {
usage = finish_usage;
match run.on_finish(reason, finish_usage).await {
step_cost = cost.map(|c| c.cost_of(&finish_usage)).unwrap_or(0.0);
match run.on_finish(reason, finish_usage, step_cost).await {
Ok(r) => {
result = r;
Ok(())
@@ -652,6 +659,7 @@ pub async fn process_step(
result,
message_id: run.assistant.as_ref().map(|m| m.id.clone()),
usage,
cost: step_cost,
aborted: false,
})
}