use crate::event_log::EventLog;
use crate::events::{Event, EventKind};
use crate::paths::MissionPaths;
use crate::reducer;
use crate::types::{
AssertionCheck, BackendKind, FeatureOrigin, MissionConfig, MissionState, MissionStatus, Plan,
PlanFeature, PlanMilestone, Role, TokenUsage, WorkerRun,
};
use serde::{Deserialize, Serialize};
use std::path::Path;
const TOKENS_PER_MTOK: f64 = 1_000_000.0;
const GPT_5_6_LONG_CONTEXT_THRESHOLD: u64 = 272_000;
pub(crate) fn resolved_run_backend(
recorded: Option<BackendKind>,
role: Role,
legacy_config: Option<&MissionConfig>,
) -> BackendKind {
recorded.unwrap_or_else(|| {
legacy_config
.map(|cfg| cfg.backend_kind(role))
.unwrap_or(BackendKind::Claude)
})
}
pub(crate) fn resolved_run_cost(
recorded: Option<f64>,
usage: &TokenUsage,
model: &str,
backend: BackendKind,
) -> f64 {
recorded.unwrap_or_else(|| usage_cost_usd_for_backend(usage, model, backend))
}
fn worker_run_cost(run: &WorkerRun, config: &MissionConfig) -> f64 {
resolved_run_cost(
run.cost_usd,
&run.tokens,
&run.model,
resolved_run_backend(run.backend, run.role, Some(config)),
)
}
pub const DEFAULT_CODEX_MODEL: &str = "gpt-5.6-sol";
pub fn is_codex_model(model: &str) -> bool {
let m = model.to_ascii_lowercase();
m.contains("codex") || m.contains("gpt")
}
pub const DEFAULT_DROID_MODEL: &str = "accounts/fireworks/models/glm-5p2";
pub fn is_droid_model(model: &str) -> bool {
let m = model.to_ascii_lowercase();
m.contains("glm") || m.contains("fireworks")
}
pub const DEFAULT_KIMI_MODEL: &str = "kimi-code/k3";
pub const DEFAULT_CURSOR_MODEL: &str = "gpt-5";
pub fn is_kimi_model(model: &str) -> bool {
let m = model.to_ascii_lowercase();
m.contains("k3") || m.contains("kimi")
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Pricing {
pub input_per_mtok: f64,
pub output_per_mtok: f64,
}
impl Pricing {
pub fn cache_read_per_mtok(&self) -> f64 {
0.1 * self.input_per_mtok
}
pub fn cache_write_per_mtok(&self) -> f64 {
1.25 * self.input_per_mtok
}
}
pub fn pricing_for_model(model: &str) -> Pricing {
let m = model.to_ascii_lowercase();
if m.contains("fable") {
Pricing {
input_per_mtok: 10.0,
output_per_mtok: 50.0,
}
} else if m == "gpt-5.6" || m.contains("gpt-5.6-sol") {
Pricing {
input_per_mtok: 4.0,
output_per_mtok: 20.0,
}
} else if m.contains("codex") || m.contains("gpt") {
Pricing {
input_per_mtok: 1.25,
output_per_mtok: 10.0,
}
} else if m.contains("glm") || m.contains("fireworks") {
Pricing {
input_per_mtok: 0.55,
output_per_mtok: 2.19,
}
} else if m.contains("k3") || m.contains("kimi") {
Pricing {
input_per_mtok: 0.60,
output_per_mtok: 2.50,
}
} else if m.contains("opus") {
Pricing {
input_per_mtok: 5.0,
output_per_mtok: 25.0,
}
} else if m.contains("sonnet") {
Pricing {
input_per_mtok: 3.0,
output_per_mtok: 15.0,
}
} else if m.contains("haiku") {
Pricing {
input_per_mtok: 1.0,
output_per_mtok: 5.0,
}
} else {
Pricing {
input_per_mtok: 5.0,
output_per_mtok: 25.0,
}
}
}
fn is_gpt_5_6_sol(model: &str) -> bool {
let m = model.to_ascii_lowercase();
m == "gpt-5.6" || m.contains("gpt-5.6-sol")
}
pub fn usage_cost_usd(usage: &TokenUsage, model: &str) -> f64 {
let p = pricing_for_model(model);
let total_input = usage
.input
.saturating_add(usage.cache_read)
.saturating_add(usage.cache_write);
let (input_multiplier, output_multiplier) =
if is_gpt_5_6_sol(model) && total_input > GPT_5_6_LONG_CONTEXT_THRESHOLD {
(2.0, 1.5)
} else {
(1.0, 1.0)
};
((usage.input as f64 / TOKENS_PER_MTOK) * p.input_per_mtok
+ (usage.cache_read as f64 / TOKENS_PER_MTOK) * p.cache_read_per_mtok()
+ (usage.cache_write as f64 / TOKENS_PER_MTOK) * p.cache_write_per_mtok())
* input_multiplier
+ (usage.output as f64 / TOKENS_PER_MTOK) * p.output_per_mtok * output_multiplier
}
pub fn usage_cost_usd_for_backend(usage: &TokenUsage, model: &str, backend: BackendKind) -> f64 {
if backend == BackendKind::Local {
0.0
} else {
usage_cost_usd(usage, model)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct EstimateParams {
pub respawn_allowance: f64,
pub fix_cycles_per_milestone: f64,
pub fix_features_per_cycle: f64,
pub avg_worker_run_usd: f64,
pub avg_validator_run_usd: f64,
pub orchestrator_overhead_usd_per_feature: f64,
}
impl Default for EstimateParams {
fn default() -> Self {
EstimateParams {
respawn_allowance: 0.2,
fix_cycles_per_milestone: 0.5,
fix_features_per_cycle: 2.0,
avg_worker_run_usd: 1.50,
avg_validator_run_usd: 0.75,
orchestrator_overhead_usd_per_feature: 0.25,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct CostEstimate {
pub worker_runs: f64,
pub validator_runs: f64,
pub low_usd: f64,
pub expected_usd: f64,
pub high_usd: f64,
pub shape: MissionShape,
pub confidence: Confidence,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Confidence {
High,
Low,
}
pub fn estimate(plan: &Plan, cfg: &MissionConfig, p: &EstimateParams) -> CostEstimate {
let milestones = plan.milestones.len() as f64;
let features = plan
.milestones
.iter()
.map(|m| m.features.len())
.sum::<usize>() as f64;
let r = p.respawn_allowance;
let x = p.fix_cycles_per_milestone;
let f = p.fix_features_per_cycle;
let pool_n = cfg.worker_candidates.len().max(1) as f64;
let worker_runs = (features * (1.0 + r) + milestones * x * f * (1.0 + r)) * pool_n;
let validators_per_milestone =
2.0 - (cfg.skip_scrutiny as u8 as f64) - (cfg.skip_functional as u8 as f64);
let validator_runs = validators_per_milestone * milestones * (1.0 + x);
let expected_usd = worker_runs * p.avg_worker_run_usd
+ validator_runs * p.avg_validator_run_usd
+ features * p.orchestrator_overhead_usd_per_feature;
CostEstimate {
worker_runs,
validator_runs,
low_usd: 0.5 * expected_usd,
expected_usd,
high_usd: 2.5 * expected_usd,
shape: MissionShape::Unknown,
confidence: Confidence::High,
}
}
pub const CACHE_MISS_MULT: f64 = 9.0;
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TwoPathEstimate {
pub local_usd: f64,
pub escalated: CostEstimate,
pub cache_miss_usd: f64,
}
pub fn estimate_two_path(
frontier: CostEstimate,
cfg: &MissionConfig,
p: &EstimateParams,
) -> Option<TwoPathEstimate> {
if !cfg.worker_candidates.is_empty() {
return None;
}
if cfg.worker.backend.as_deref() != Some("local") {
return None;
}
let cache_miss_usd = p.avg_worker_run_usd * CACHE_MISS_MULT;
let mut escalated = frontier;
escalated.expected_usd += cache_miss_usd;
escalated.low_usd += cache_miss_usd;
escalated.high_usd += cache_miss_usd;
Some(TwoPathEstimate {
local_usd: 0.0,
escalated,
cache_miss_usd,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum MissionShape {
CodeChange,
DocHeavy,
Unknown,
}
pub fn classify_shape(plan: &Plan) -> MissionShape {
const BUILD_TEST_TOKENS: &[&str] = &[
"cargo test",
"cargo build",
"cargo check",
"cargo clippy",
"npm test",
"npm run",
"pytest",
"go test",
"make ",
];
let judgements = plan
.validation_contract
.iter()
.filter(|a| a.check == AssertionCheck::AgentJudgement)
.count();
let build_test_cmd = plan.validation_contract.iter().any(|a| {
a.check == AssertionCheck::Command
&& a.command
.as_deref()
.map(|c| {
let lower = c.to_ascii_lowercase();
BUILD_TEST_TOKENS.iter().any(|tok| lower.contains(tok))
})
.unwrap_or(false)
});
if build_test_cmd {
MissionShape::CodeChange
} else if judgements >= 1 {
MissionShape::DocHeavy
} else {
MissionShape::Unknown
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Calibration {
pub params: EstimateParams,
pub missions_used: usize,
pub doc_heavy_missions_used: usize,
pub expected_mult: f64,
pub low_mult: f64,
pub high_mult: f64,
pub excluded_non_frontier: usize,
pub gate_correlation: Option<f64>,
}
pub const MIN_CALIBRATION_MISSIONS: usize = 5;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MissionCostClass {
Frontier,
Local,
Mixed,
}
pub fn mission_cost_class(state: &MissionState) -> MissionCostClass {
let still_local = state.config.worker.backend.as_deref() == Some("local")
&& state.config.worker_candidates.is_empty();
let escalated = state.escalated_milestones > 0;
match (still_local, escalated) {
(true, false) => MissionCostClass::Local,
(false, false) => MissionCostClass::Frontier,
_ => MissionCostClass::Mixed,
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct GateActivity {
pub blocked: u32,
pub grant_requests: u32,
pub fix_features: u32,
pub resumes: u32,
}
impl GateActivity {
pub fn score(&self) -> f64 {
(self.blocked + self.grant_requests + self.fix_features + self.resumes) as f64
}
}
pub fn gate_activity(events: &[Event]) -> GateActivity {
let mut activity = GateActivity::default();
for event in events {
match &event.kind {
EventKind::MilestoneBlocked { .. } => activity.blocked += 1,
EventKind::GrantRequested { .. } => activity.grant_requests += 1,
EventKind::FixFeatureCreated { .. } => activity.fix_features += 1,
EventKind::MissionResumed {} => activity.resumes += 1,
_ => {}
}
}
activity
}
fn pearson(xs: &[f64], ys: &[f64]) -> Option<f64> {
if xs.len() != ys.len() || xs.len() < 2 {
return None;
}
let n = xs.len() as f64;
let mean = |v: &[f64]| v.iter().sum::<f64>() / n;
let (mx, my) = (mean(xs), mean(ys));
let mut cov = 0.0;
let (mut vx, mut vy) = (0.0, 0.0);
for i in 0..xs.len() {
cov += (xs[i] - mx) * (ys[i] - my);
vx += (xs[i] - mx) * (xs[i] - mx);
vy += (ys[i] - my) * (ys[i] - my);
}
(vx > 0.0 && vy > 0.0).then(|| cov / vx.sqrt() / vy.sqrt())
}
pub fn calibrate(repo_root: &Path) -> Calibration {
let mut per_mission: Vec<EstimateParams> = Vec::new();
let mut doc_heavy_missions_used = 0usize;
let mut excluded_non_frontier = 0usize;
let mut corpus: Vec<(usize, usize, MissionConfig, f64, f64)> = Vec::new();
for mission_id in MissionPaths::list_missions(repo_root) {
let paths = MissionPaths::new(repo_root, &mission_id);
let Ok(events) = EventLog::read_events(&paths.events_file()) else {
continue; };
let Ok(state) = reducer::fold(&events) else {
continue; };
if state.mission.status != MissionStatus::Complete {
continue;
}
if mission_cost_class(&state) != MissionCostClass::Frontier {
excluded_non_frontier += 1;
continue;
}
if classify_shape(&mission_plan(&state)) == MissionShape::DocHeavy {
doc_heavy_missions_used += 1;
}
per_mission.push(mission_actuals(&state));
let milestones = state.mission.milestones.len();
let planned_features = state
.mission
.milestones
.iter()
.flat_map(|m| m.features.iter())
.filter(|f| f.origin == FeatureOrigin::Plan)
.count();
corpus.push((
milestones,
planned_features,
state.config.clone(),
mission_total_cost(&state),
gate_activity(&events).score(),
));
}
if per_mission.is_empty() {
return Calibration {
params: EstimateParams::default(),
missions_used: 0,
doc_heavy_missions_used: 0,
expected_mult: 1.0,
low_mult: 0.5,
high_mult: 2.5,
excluded_non_frontier,
gate_correlation: None,
};
}
let n = per_mission.len() as f64;
let mean = |get: fn(&EstimateParams) -> f64| per_mission.iter().map(get).sum::<f64>() / n;
let params = EstimateParams {
respawn_allowance: mean(|p| p.respawn_allowance).max(0.0),
fix_cycles_per_milestone: mean(|p| p.fix_cycles_per_milestone).max(0.0),
fix_features_per_cycle: mean(|p| p.fix_features_per_cycle).max(0.0),
avg_worker_run_usd: mean(|p| p.avg_worker_run_usd).max(0.01),
avg_validator_run_usd: mean(|p| p.avg_validator_run_usd).max(0.01),
orchestrator_overhead_usd_per_feature: mean(|p| p.orchestrator_overhead_usd_per_feature)
.max(0.01),
};
let (expected_mult, low_mult, high_mult, gate_correlation) =
fit_estimate_to_corpus(¶ms, &corpus);
Calibration {
params,
missions_used: per_mission.len(),
doc_heavy_missions_used,
expected_mult,
low_mult,
high_mult,
excluded_non_frontier,
gate_correlation,
}
}
fn mission_total_cost(state: &MissionState) -> f64 {
state
.runs
.values()
.map(|run| worker_run_cost(run, &state.config))
.sum()
}
fn fit_estimate_to_corpus(
params: &EstimateParams,
corpus: &[(usize, usize, MissionConfig, f64, f64)],
) -> (f64, f64, f64, Option<f64>) {
const DEFAULT: (f64, f64, f64, Option<f64>) = (1.0, 0.5, 2.5, None);
if corpus.len() < MIN_CALIBRATION_MISSIONS {
return DEFAULT;
}
let mut sum_pred = 0.0;
let mut sum_actual = 0.0;
let mut ratios: Vec<f64> = Vec::new();
let mut gate_scores: Vec<f64> = Vec::new();
for (milestones, features, cfg, actual, gate_score) in corpus {
let pred = estimate(&counts_plan(*milestones, *features), cfg, params).expected_usd;
if pred > 0.0 && *actual > 0.0 {
sum_pred += pred;
sum_actual += *actual;
ratios.push(*actual / pred);
gate_scores.push(*gate_score);
}
}
if ratios.len() < MIN_CALIBRATION_MISSIONS || sum_pred <= 0.0 {
return DEFAULT;
}
let gate_correlation = pearson(&gate_scores, &ratios);
ratios.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let center = (sum_actual / sum_pred).clamp(0.1, 20.0);
let low = percentile(&ratios, 0.10).min(0.5).min(center).max(0.02);
let high = percentile(&ratios, 0.90)
.max(2.5)
.max(center)
.min(center.max(1.0) * 8.0);
(center, low, high, gate_correlation)
}
fn counts_plan(milestones: usize, features: usize) -> Plan {
let milestones = milestones.max(1);
let mut ms = Vec::with_capacity(milestones);
for i in 0..milestones {
let n = if i == 0 { features } else { 0 };
ms.push(PlanMilestone {
title: String::new(),
features: (0..n)
.map(|_| PlanFeature {
title: String::new(),
spec: String::new(),
validation_criteria: Vec::new(),
})
.collect(),
});
}
Plan {
goal: String::new(),
validation_contract: Vec::new(),
milestones: ms,
considered_alternatives: None,
command_grants: Vec::new(),
touch_set: Vec::new(),
standards_manifest: None,
reviewer_independence: None,
}
}
fn percentile(sorted: &[f64], q: f64) -> f64 {
if sorted.is_empty() {
return 0.0;
}
let idx = (q * (sorted.len() as f64 - 1.0)).round() as usize;
sorted[idx.min(sorted.len() - 1)]
}
fn mission_plan(state: &MissionState) -> Plan {
Plan {
goal: state.mission.goal.clone(),
considered_alternatives: None,
command_grants: state.mission.command_grants.clone(),
touch_set: state.mission.touch_set.clone(),
standards_manifest: state.mission.standards_manifest.clone().map(Box::new),
reviewer_independence: state.mission.reviewer_independence,
validation_contract: state.mission.validation_contract.clone(),
milestones: state
.mission
.milestones
.iter()
.map(|m| PlanMilestone {
title: m.title.clone(),
features: m
.features
.iter()
.map(|f| PlanFeature {
title: f.title.clone(),
spec: f.spec.clone(),
validation_criteria: f.validation_criteria.clone(),
})
.collect(),
})
.collect(),
}
}
pub fn apply_shape(base: CostEstimate, plan: &Plan, cal: &Calibration) -> CostEstimate {
let shape = classify_shape(plan);
let mut est = base;
est.shape = shape;
let raw = est.expected_usd;
est.expected_usd = raw * cal.expected_mult;
est.low_usd = raw * cal.low_mult;
est.high_usd = raw * cal.high_mult;
if shape == MissionShape::DocHeavy && cal.doc_heavy_missions_used == 0 {
est.confidence = Confidence::Low;
est.high_usd = est
.high_usd
.max(est.expected_usd * LOW_CONFIDENCE_HIGH_MULT);
} else {
est.confidence = Confidence::High;
}
est
}
const LOW_CONFIDENCE_HIGH_MULT: f64 = 15.0;
fn mission_actuals(state: &MissionState) -> EstimateParams {
let run_cost = |run: &WorkerRun| worker_run_cost(run, &state.config);
let mean_run_cost = |roles: &[Role]| -> f64 {
let costs: Vec<f64> = state
.runs
.values()
.filter(|r| roles.contains(&r.role))
.map(run_cost)
.collect();
if costs.is_empty() {
0.0
} else {
costs.iter().sum::<f64>() / costs.len() as f64
}
};
let features = || {
state
.mission
.milestones
.iter()
.flat_map(|m| m.features.iter())
};
let total_features = features().count() as f64;
let planned_features = features()
.filter(|f| f.origin == FeatureOrigin::Plan)
.count() as f64;
let fix_features = features()
.filter(|f| f.origin == FeatureOrigin::Fix)
.count() as f64;
let total_respawns = features().map(|f| f.respawns as f64).sum::<f64>();
let milestones = state.mission.milestones.len() as f64;
let total_fix_cycles = state
.mission
.milestones
.iter()
.map(|m| m.fix_cycles as f64)
.sum::<f64>();
let orchestrator_total = state
.runs
.values()
.filter(|r| r.role == Role::Orchestrator)
.map(run_cost)
.sum::<f64>();
let safe_div = |num: f64, den: f64| if den > 0.0 { num / den } else { 0.0 };
EstimateParams {
respawn_allowance: safe_div(total_respawns, planned_features),
fix_cycles_per_milestone: safe_div(total_fix_cycles, milestones),
fix_features_per_cycle: fix_features / total_fix_cycles.max(1.0),
avg_worker_run_usd: mean_run_cost(&[Role::Worker]),
avg_validator_run_usd: mean_run_cost(&[Role::ValidatorScrutiny, Role::ValidatorFunctional]),
orchestrator_overhead_usd_per_feature: safe_div(orchestrator_total, total_features),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn codex_pricing_applied() {
let codex = pricing_for_model(DEFAULT_CODEX_MODEL);
assert_eq!(codex.input_per_mtok, 4.0);
assert_eq!(codex.output_per_mtok, 20.0);
let opus = pricing_for_model("opus");
assert_ne!(codex, opus);
let usage = TokenUsage {
input: 2_000_000,
output: 1_000_000,
cache_read: 500_000,
cache_write: 200_000,
};
let expected =
(2.0 * 4.0 + 0.5 * (0.1 * 4.0) + 0.2 * (1.25 * 4.0)) * 2.0 + 1.0 * 20.0 * 1.5;
let got = usage_cost_usd(&usage, DEFAULT_CODEX_MODEL);
assert!(
(got - expected).abs() < 1e-9,
"got {got}, expected {expected}"
);
}
#[test]
fn gpt_5_6_alias_uses_sol_pricing() {
assert_eq!(
pricing_for_model("gpt-5.6"),
pricing_for_model("gpt-5.6-sol")
);
}
#[test]
fn gpt_5_6_sol_long_context_multiplier_starts_above_272k() {
let at_threshold = TokenUsage {
input: 200_000,
cache_read: 72_000,
cache_write: 0,
output: 10_000,
};
let base = 0.2 * 4.0 + 0.072 * 0.4 + 0.01 * 20.0;
assert!((usage_cost_usd(&at_threshold, "gpt-5.6-sol") - base).abs() < 1e-9);
let above_threshold = TokenUsage {
input: 200_001,
..at_threshold
};
let long = (0.200001 * 4.0 + 0.072 * 0.4) * 2.0 + 0.01 * 20.0 * 1.5;
assert!((usage_cost_usd(&above_threshold, "gpt-5.6-sol") - long).abs() < 1e-9);
assert!((usage_cost_usd(&above_threshold, "gpt-5.6") - long).abs() < 1e-9);
}
#[test]
fn unknown_model_falls_back_to_opus_tier() {
let unknown = pricing_for_model("some-unknown-model-xyz");
let opus = pricing_for_model("opus");
assert_eq!(unknown, opus);
}
#[test]
fn droid_pricing_applied() {
let glm = pricing_for_model(DEFAULT_DROID_MODEL);
assert_eq!(glm.input_per_mtok, 0.55);
assert_eq!(glm.output_per_mtok, 2.19);
let opus = pricing_for_model("opus");
let codex = pricing_for_model(DEFAULT_CODEX_MODEL);
assert_ne!(glm, opus);
assert_ne!(glm, codex);
let usage = TokenUsage {
input: 2_000_000,
output: 1_000_000,
cache_read: 500_000,
cache_write: 200_000,
};
let expected = 2.0 * 0.55 + 1.0 * 2.19 + 0.5 * (0.1 * 0.55) + 0.2 * (1.25 * 0.55);
let got = usage_cost_usd(&usage, DEFAULT_DROID_MODEL);
assert!(
(got - expected).abs() < 1e-9,
"got {got}, expected {expected}"
);
let fable = pricing_for_model("claude-fable-5");
assert_eq!(fable.input_per_mtok, 10.0);
assert_eq!(fable.output_per_mtok, 50.0);
}
#[test]
fn kimi_pricing_applied() {
let kimi = pricing_for_model(DEFAULT_KIMI_MODEL);
assert_eq!(kimi.input_per_mtok, 0.60);
assert_eq!(kimi.output_per_mtok, 2.50);
let opus = pricing_for_model("opus");
let codex = pricing_for_model(DEFAULT_CODEX_MODEL);
let droid = pricing_for_model(DEFAULT_DROID_MODEL);
assert_ne!(kimi, opus);
assert_ne!(kimi, codex);
assert_ne!(kimi, droid);
assert!(is_kimi_model("kimi-code/k3"));
assert!(is_kimi_model("K3"));
assert!(!is_kimi_model("opus"));
let usage = TokenUsage {
input: 2_000_000,
output: 1_000_000,
cache_read: 500_000,
cache_write: 200_000,
};
let expected = 2.0 * 0.60 + 1.0 * 2.50 + 0.5 * (0.1 * 0.60) + 0.2 * (1.25 * 0.60);
let got = usage_cost_usd(&usage, DEFAULT_KIMI_MODEL);
assert!(
(got - expected).abs() < 1e-9,
"got {got}, expected {expected}"
);
}
#[test]
fn local_usage_cost_is_always_zero() {
let usage = TokenUsage {
input: 2_000_000,
output: 1_000_000,
cache_read: 500_000,
cache_write: 200_000,
};
assert_eq!(
usage_cost_usd_for_backend(&usage, "my-local-model", BackendKind::Local),
0.0
);
assert_eq!(
usage_cost_usd_for_backend(&usage, "anything-at-all", BackendKind::Local),
0.0
);
}
#[test]
fn local_usage_cost_does_not_change_other_backend_pricing() {
let usage = TokenUsage {
input: 2_000_000,
output: 1_000_000,
cache_read: 500_000,
cache_write: 200_000,
};
for (backend, model) in [
(BackendKind::Claude, "sonnet"),
(BackendKind::Codex, DEFAULT_CODEX_MODEL),
(BackendKind::Droid, DEFAULT_DROID_MODEL),
(BackendKind::Kimi, DEFAULT_KIMI_MODEL),
] {
assert_eq!(
usage_cost_usd_for_backend(&usage, model, backend),
usage_cost_usd(&usage, model),
"backend {backend:?} pricing should be unchanged"
);
}
}
use crate::event_log::{EventLog, LockForce};
use crate::events::{Event, EventKind};
use crate::paths::MissionPaths;
use std::time::Duration;
fn seed_mission(repo_root: &Path, id: &str, kinds: Vec<EventKind>) {
let paths = MissionPaths::new(repo_root, id);
let mut log = EventLog::acquire(&paths, id, Duration::ZERO, LockForce::No).unwrap();
for kind in kinds {
log.append(kind).unwrap();
}
}
fn created_with(config: MissionConfig) -> EventKind {
EventKind::MissionCreated {
goal: "g".into(),
base_branch: "main".into(),
mission_branch: "kranz/mission-x".into(),
config,
}
}
fn local_config() -> MissionConfig {
let mut cfg = MissionConfig::default();
cfg.worker.backend = Some("local".to_string());
cfg
}
fn approved_and_completed() -> Vec<EventKind> {
vec![
EventKind::PlanApproved {
plan: crate::types::Plan {
goal: "g".into(),
validation_contract: vec![],
milestones: vec![crate::types::PlanMilestone {
title: "milestone one".into(),
features: vec![crate::types::PlanFeature {
title: "alpha".into(),
spec: "build alpha".into(),
validation_criteria: vec![],
}],
}],
considered_alternatives: None,
command_grants: vec![],
touch_set: vec![],
standards_manifest: None,
reviewer_independence: None,
},
base_sha: None,
},
EventKind::MissionCompleted {},
]
}
#[test]
fn mission_cost_class_maps_local_mixed_and_frontier() {
let cases = [
(MissionConfig::default(), false, MissionCostClass::Frontier),
(local_config(), false, MissionCostClass::Local),
(local_config(), true, MissionCostClass::Mixed),
];
for (config, escalate, expected) in cases {
let mut kinds = vec![created_with(config)];
kinds.extend(approved_and_completed());
if escalate {
kinds.insert(
kinds.len() - 1,
EventKind::TierEscalated {
milestone_id: "ms-1".into(),
from: crate::types::ExecutorTier::Local,
to: crate::types::ExecutorTier::Frontier,
reason: "two failed local validations".into(),
},
);
}
let events: Vec<Event> = kinds
.into_iter()
.enumerate()
.map(|(i, kind)| Event {
seq: (i + 1) as u64,
ts: chrono::Utc::now(),
mission_id: "m-1".into(),
kind,
})
.collect();
let state = crate::reducer::fold(&events).unwrap();
assert_eq!(mission_cost_class(&state), expected, "escalate={escalate}");
}
}
#[test]
fn calibrate_excludes_local_and_mixed_from_the_frontier_corpus() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let mut frontier = vec![created_with(MissionConfig::default())];
frontier.extend(approved_and_completed());
seed_mission(root, "m-frontier", frontier);
let mut local = vec![created_with(local_config())];
local.extend(approved_and_completed());
seed_mission(root, "m-local", local);
let mut mixed = vec![created_with(local_config())];
mixed.extend(approved_and_completed());
mixed.insert(
mixed.len() - 1,
EventKind::TierEscalated {
milestone_id: "ms-1".into(),
from: crate::types::ExecutorTier::Local,
to: crate::types::ExecutorTier::Frontier,
reason: "two failed local validations".into(),
},
);
seed_mission(root, "m-mixed", mixed);
let calibration = calibrate(root);
assert_eq!(calibration.missions_used, 1, "frontier missions only");
assert_eq!(calibration.excluded_non_frontier, 2);
}
#[test]
fn gate_activity_counts_blocked_grants_fixes_and_resumes() {
let events = vec![
Event {
seq: 1,
ts: chrono::Utc::now(),
mission_id: "m-1".into(),
kind: EventKind::MilestoneBlocked {
block_context: None,
milestone_id: "ms-1".into(),
reason: "r".into(),
},
},
Event {
seq: 2,
ts: chrono::Utc::now(),
mission_id: "m-1".into(),
kind: EventKind::GrantRequested {
milestone_id: "ms-1".into(),
kind: crate::types::GrantKind::Command,
command: "cargo test".into(),
},
},
Event {
seq: 3,
ts: chrono::Utc::now(),
mission_id: "m-1".into(),
kind: EventKind::FixFeatureCreated {
milestone_id: "ms-1".into(),
feature: crate::types::Feature {
id: "ms-1-fix-1-1".into(),
title: "fix".into(),
spec: "s".into(),
validation_criteria: vec![],
origin: crate::types::FeatureOrigin::Fix,
status: crate::types::FeatureStatus::Pending,
worker_runs: vec![],
commits: vec![],
respawns: 0,
},
},
},
Event {
seq: 4,
ts: chrono::Utc::now(),
mission_id: "m-1".into(),
kind: EventKind::MissionResumed {},
},
Event {
seq: 5,
ts: chrono::Utc::now(),
mission_id: "m-1".into(),
kind: EventKind::MissionCompleted {},
},
];
let activity = gate_activity(&events);
assert_eq!(activity.blocked, 1);
assert_eq!(activity.grant_requests, 1);
assert_eq!(activity.fix_features, 1);
assert_eq!(activity.resumes, 1);
assert_eq!(activity.score(), 4.0);
}
#[test]
fn fit_widens_to_cover_a_gate_heavy_outlier_and_reports_correlation() {
let params = EstimateParams::default();
let cfg = MissionConfig::default();
let mut corpus: Vec<(usize, usize, MissionConfig, f64, f64)> = Vec::new();
for _ in 0..4 {
let pred = estimate(&counts_plan(1, 2), &cfg, ¶ms).expected_usd;
corpus.push((1, 2, cfg.clone(), pred, 1.0)); }
let pred = estimate(&counts_plan(1, 2), &cfg, ¶ms).expected_usd;
corpus.push((1, 2, cfg.clone(), pred * 4.0, 13.0));
let (center, _low, high, gate_correlation) = fit_estimate_to_corpus(¶ms, &corpus);
assert!(high >= 4.0, "p90 must cover the 4x outlier: high={high}");
assert!(
center < 2.0,
"the recentered middle must not chase the outlier: {center}"
);
let r = gate_correlation.expect("correlation defined with variance");
assert!(r > 0.9, "gate activity tracks the overrun: r={r}");
}
#[test]
fn two_path_prices_the_miss_once_and_only_for_local_routes() {
let p = EstimateParams::default();
let base = CostEstimate {
worker_runs: 4.0,
validator_runs: 2.0,
low_usd: 5.0,
expected_usd: 10.0,
high_usd: 25.0,
shape: MissionShape::Unknown,
confidence: Confidence::High,
};
assert!(estimate_two_path(base, &MissionConfig::default(), &p).is_none());
let local_cfg = local_config();
let two = estimate_two_path(base, &local_cfg, &p).unwrap();
let miss = p.avg_worker_run_usd * CACHE_MISS_MULT;
assert_eq!(two.local_usd, 0.0, "completes-locally is $0 marginal");
assert_eq!(two.cache_miss_usd, miss);
assert_eq!(
two.escalated.expected_usd,
10.0 + miss,
"the miss is priced once per escalation, never per turn"
);
assert_eq!(two.escalated.low_usd, 5.0 + miss);
assert_eq!(two.escalated.high_usd, 25.0 + miss);
}
fn dispatch_pool_config() -> MissionConfig {
MissionConfig {
worker_candidates: vec![
crate::types::CandidateSpec {
backend: "claude".into(),
model: "sonnet".into(),
},
crate::types::CandidateSpec {
backend: "codex".into(),
model: DEFAULT_CODEX_MODEL.into(),
},
],
..MissionConfig::default()
}
}
#[test]
fn dispatch_pool_estimate_multiplies_worker_runs_only() {
let plan = counts_plan(1, 2);
let p = EstimateParams::default();
let single = estimate(&plan, &MissionConfig::default(), &p);
let pooled = estimate(&plan, &dispatch_pool_config(), &p);
assert_eq!(pooled.worker_runs, single.worker_runs * 2.0);
assert_eq!(pooled.validator_runs, single.validator_runs);
let expected = single.worker_runs * 2.0 * p.avg_worker_run_usd
+ single.validator_runs * p.avg_validator_run_usd
+ 2.0 * p.orchestrator_overhead_usd_per_feature;
assert!(
(pooled.expected_usd - expected).abs() < 1e-9,
"pooled {} vs hand-computed {expected}",
pooled.expected_usd
);
assert!(pooled.expected_usd > single.expected_usd);
assert!(pooled.high_usd > single.high_usd);
let delta = pooled.expected_usd - single.expected_usd;
assert!(
(delta - single.worker_runs * p.avg_worker_run_usd).abs() < 1e-9,
"delta {delta} should be exactly one more worker-share ({})",
single.worker_runs * p.avg_worker_run_usd
);
}
#[test]
fn dispatch_pool_two_path_suppressed_and_cost_class_frontier() {
let p = EstimateParams::default();
let mut cfg = dispatch_pool_config();
cfg.worker.backend = Some("local".to_string());
let base = estimate(&counts_plan(1, 1), &cfg, &p);
assert!(estimate_two_path(base, &cfg, &p).is_none());
let mut kinds = vec![created_with(cfg)];
kinds.extend(approved_and_completed());
let events: Vec<Event> = kinds
.into_iter()
.enumerate()
.map(|(i, kind)| Event {
seq: (i + 1) as u64,
ts: chrono::Utc::now(),
mission_id: "m-1".into(),
kind,
})
.collect();
let state = crate::reducer::fold(&events).unwrap();
assert_eq!(mission_cost_class(&state), MissionCostClass::Frontier);
assert_eq!(state.executor_tier(), crate::types::ExecutorTier::Frontier);
}
}
#[cfg(test)]
#[path = "resolved_accounting_tests.rs"]
mod resolved_accounting_tests;