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:
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user