use cersei::events::AgentEvent;
use cersei::prelude::*;
use cersei::provider::{CompletionStream, ProviderCapabilities, ProviderOptions};
use cersei::reporters::{AgentMetrics, MetricsReporter, Reporter};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::mpsc;
const SONNET_INPUT_PER_MTOK: f64 = 3.00; const SONNET_OUTPUT_PER_MTOK: f64 = 15.00; const SONNET_CACHE_WRITE_PER_MTOK: f64 = 3.75;
const SONNET_CACHE_READ_PER_MTOK: f64 = 0.30;
const OPUS_INPUT_PER_MTOK: f64 = 15.00;
const OPUS_OUTPUT_PER_MTOK: f64 = 75.00;
const HAIKU_INPUT_PER_MTOK: f64 = 0.80;
const HAIKU_OUTPUT_PER_MTOK: f64 = 4.00;
fn compute_cost(model: &str, input_tokens: u64, output_tokens: u64) -> f64 {
let (input_rate, output_rate) = if model.contains("opus") {
(OPUS_INPUT_PER_MTOK, OPUS_OUTPUT_PER_MTOK)
} else if model.contains("haiku") {
(HAIKU_INPUT_PER_MTOK, HAIKU_OUTPUT_PER_MTOK)
} else {
(SONNET_INPUT_PER_MTOK, SONNET_OUTPUT_PER_MTOK)
};
(input_tokens as f64 / 1_000_000.0) * input_rate
+ (output_tokens as f64 / 1_000_000.0) * output_rate
}
struct SimulatedClaude {
model: String,
turn: Arc<std::sync::atomic::AtomicU32>,
}
impl SimulatedClaude {
fn new(model: &str) -> Self {
Self {
model: model.to_string(),
turn: Arc::new(std::sync::atomic::AtomicU32::new(0)),
}
}
}
#[async_trait]
impl Provider for SimulatedClaude {
fn name(&self) -> &str {
"simulated-claude"
}
fn context_window(&self, _model: &str) -> u64 {
200_000
}
fn capabilities(&self, _model: &str) -> ProviderCapabilities {
ProviderCapabilities {
streaming: true,
tool_use: true,
vision: true,
thinking: true,
system_prompt: true,
caching: true,
}
}
async fn complete(&self, request: CompletionRequest) -> cersei_types::Result<CompletionStream> {
let turn = self.turn.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
let model = self.model.clone();
let msg_count = request.messages.len();
let base_input = 1200 + (msg_count as u64 * 350); let cache_read = if turn > 0 { base_input / 3 } else { 0 };
let (tx, rx) = mpsc::channel(64);
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(50)).await;
let _ = tx
.send(StreamEvent::MessageStart {
id: format!("msg_{}", uuid::Uuid::new_v4()),
model: model.clone(),
})
.await;
match turn {
0 => {
let _ = tx
.send(StreamEvent::ContentBlockStart {
index: 0,
block_type: "thinking".into(),
id: None,
name: None,
})
.await;
let _ = tx
.send(StreamEvent::ThinkingDelta {
index: 0,
thinking: "Let me look at the project structure first...".into(),
})
.await;
let _ = tx.send(StreamEvent::ContentBlockStop { index: 0 }).await;
let _ = tx
.send(StreamEvent::ContentBlockStart {
index: 1,
block_type: "text".into(),
id: None,
name: None,
})
.await;
let _ = tx
.send(StreamEvent::TextDelta {
index: 1,
text: "I'll start by examining the project structure.".into(),
})
.await;
let _ = tx.send(StreamEvent::ContentBlockStop { index: 1 }).await;
let _ = tx
.send(StreamEvent::ContentBlockStart {
index: 2,
block_type: "tool_use".into(),
id: None,
name: None,
})
.await;
let _ = tx
.send(StreamEvent::InputJsonDelta {
index: 2,
partial_json: r#"{"pattern": "**/*.rs"}"#.into(),
})
.await;
let _ = tx.send(StreamEvent::ContentBlockStop { index: 2 }).await;
let output_tokens = 185;
let cost = compute_cost(&model, base_input, output_tokens);
let _ = tx
.send(StreamEvent::MessageDelta {
stop_reason: Some(StopReason::ToolUse),
usage: Some(Usage {
input_tokens: base_input - cache_read,
output_tokens,
total_tokens: base_input + output_tokens,
cost_usd: Some(cost),
provider_usage: serde_json::json!({
"cache_creation_input_tokens": 800,
"cache_read_input_tokens": cache_read,
}),
}),
})
.await;
}
1 => {
let _ = tx
.send(StreamEvent::ContentBlockStart {
index: 0,
block_type: "text".into(),
id: None,
name: None,
})
.await;
let _ = tx
.send(StreamEvent::TextDelta {
index: 0,
text: "Let me read the main source file.".into(),
})
.await;
let _ = tx.send(StreamEvent::ContentBlockStop { index: 0 }).await;
let _ = tx
.send(StreamEvent::ContentBlockStart {
index: 1,
block_type: "tool_use".into(),
id: None,
name: None,
})
.await;
let _ = tx
.send(StreamEvent::InputJsonDelta {
index: 1,
partial_json: r#"{"file_path": "src/main.rs"}"#.into(),
})
.await;
let _ = tx.send(StreamEvent::ContentBlockStop { index: 1 }).await;
let output_tokens = 142;
let cost = compute_cost(&model, base_input, output_tokens);
let _ = tx
.send(StreamEvent::MessageDelta {
stop_reason: Some(StopReason::ToolUse),
usage: Some(Usage {
input_tokens: base_input - cache_read,
output_tokens,
total_tokens: base_input + output_tokens,
cost_usd: Some(cost),
provider_usage: serde_json::json!({
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": cache_read,
}),
}),
})
.await;
}
_ => {
let _ = tx
.send(StreamEvent::ContentBlockStart {
index: 0,
block_type: "thinking".into(),
id: None,
name: None,
})
.await;
let _ = tx
.send(StreamEvent::ThinkingDelta {
index: 0,
thinking: "I've analyzed the project. Let me summarize my findings..."
.into(),
})
.await;
let _ = tx.send(StreamEvent::ContentBlockStop { index: 0 }).await;
let _ = tx
.send(StreamEvent::ContentBlockStart {
index: 1,
block_type: "text".into(),
id: None,
name: None,
})
.await;
let response = "Based on my analysis of the project:\n\n\
- **Structure**: 9 crates in a Cargo workspace\n\
- **Total files**: 24 Rust source files\n\
- **Lines of code**: ~3,200 lines\n\
- **Key crate**: `cersei-agent` (agent builder + agentic loop)\n\
- **Architecture**: Provider-agnostic with trait-based extensibility\n\n\
The project is well-organized with clean separation of concerns.";
for chunk in response.as_bytes().chunks(40) {
let text = String::from_utf8_lossy(chunk).to_string();
let _ = tx.send(StreamEvent::TextDelta { index: 1, text }).await;
tokio::time::sleep(Duration::from_millis(5)).await;
}
let _ = tx.send(StreamEvent::ContentBlockStop { index: 1 }).await;
let output_tokens = 387;
let cost = compute_cost(&model, base_input, output_tokens);
let _ = tx
.send(StreamEvent::MessageDelta {
stop_reason: Some(StopReason::EndTurn),
usage: Some(Usage {
input_tokens: base_input - cache_read,
output_tokens,
total_tokens: base_input + output_tokens,
cost_usd: Some(cost),
provider_usage: serde_json::json!({
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": cache_read,
}),
}),
})
.await;
}
}
let _ = tx.send(StreamEvent::MessageStop).await;
});
Ok(CompletionStream::new(rx))
}
}
#[derive(Clone)]
struct UsageTracker {
turns: Arc<parking_lot::Mutex<Vec<TurnUsage>>>,
tool_calls: Arc<parking_lot::Mutex<Vec<ToolCallInfo>>>,
}
#[derive(Clone, Debug)]
struct TurnUsage {
turn: u32,
input_tokens: u64,
output_tokens: u64,
cost_usd: f64,
stop_reason: String,
}
#[derive(Clone, Debug)]
struct ToolCallInfo {
turn: u32,
name: String,
duration_ms: f64,
is_error: bool,
}
impl UsageTracker {
fn new() -> Self {
Self {
turns: Arc::new(parking_lot::Mutex::new(Vec::new())),
tool_calls: Arc::new(parking_lot::Mutex::new(Vec::new())),
}
}
}
#[async_trait]
impl Reporter for UsageTracker {
async fn on_event(&self, event: &AgentEvent) {
match event {
AgentEvent::TurnComplete {
turn,
stop_reason,
usage,
..
} => {
self.turns.lock().push(TurnUsage {
turn: *turn,
input_tokens: usage.input_tokens,
output_tokens: usage.output_tokens,
cost_usd: usage.cost_usd.unwrap_or(0.0),
stop_reason: format!("{:?}", stop_reason),
});
}
AgentEvent::ToolEnd {
name,
duration,
is_error,
..
} => {
let turn = self.turns.lock().len() as u32 + 1;
self.tool_calls.lock().push(ToolCallInfo {
turn,
name: name.clone(),
duration_ms: duration.as_secs_f64() * 1000.0,
is_error: *is_error,
});
}
_ => {}
}
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let models = [
"claude-sonnet-4-6",
"claude-opus-4-6",
"claude-haiku-4-5-20251001",
];
let active_model = models[0];
let tracker = UsageTracker::new();
let tracker_ref = tracker.clone(); let start = Instant::now();
let agent = Agent::builder()
.provider(SimulatedClaude::new(active_model))
.tools(cersei::tools::filesystem())
.system_prompt("You are a code analyst. Examine the project and summarize it.")
.max_turns(5)
.permission_policy(AllowAll)
.working_dir(".")
.reporter(tracker_ref)
.on_event(|e| {
if let AgentEvent::TextDelta(t) = e {
print!("{}", t);
}
})
.build()?;
let output = agent
.run("Analyze this project's structure and give me a summary.")
.await?;
let elapsed = start.elapsed();
println!("\n");
println!("{}", "=".repeat(64));
println!(" CERSEI USAGE REPORT");
println!("{}", "=".repeat(64));
println!();
println!(" Model & Session");
println!(" ---------------");
println!(" Model: {}", active_model);
println!(" Provider: simulated-claude");
println!(" Wall time: {:.2}s", elapsed.as_secs_f64());
println!(" Turns: {}", output.turns);
println!(" Tool calls: {}", output.tool_calls.len());
println!(" Stop reason: {:?}", output.stop_reason);
println!();
println!(" Token Usage");
println!(" -----------");
println!(" Input tokens: {:>8}", output.usage.input_tokens);
println!(" Output tokens: {:>8}", output.usage.output_tokens);
println!(
" Total tokens: {:>8}",
output.usage.input_tokens + output.usage.output_tokens
);
println!(
" Cost (USD): ${:.6}",
output.usage.cost_usd.unwrap_or(0.0)
);
println!();
let turns = tracker.turns.lock().clone();
println!(" Per-Turn Breakdown");
println!(" ------------------");
println!(
" {:<6} {:>10} {:>10} {:>10} {:<12}",
"Turn", "Input", "Output", "Cost", "Stop"
);
println!(" {}", "-".repeat(54));
for t in &turns {
println!(
" {:<6} {:>10} {:>10} ${:>9.6} {:<12}",
t.turn, t.input_tokens, t.output_tokens, t.cost_usd, t.stop_reason
);
}
let total_cost: f64 = turns.iter().map(|t| t.cost_usd).sum();
let total_in: u64 = turns.iter().map(|t| t.input_tokens).sum();
let total_out: u64 = turns.iter().map(|t| t.output_tokens).sum();
println!(" {}", "-".repeat(54));
println!(
" {:<6} {:>10} {:>10} ${:>9.6}",
"TOTAL", total_in, total_out, total_cost
);
println!();
let tool_calls = tracker.tool_calls.lock().clone();
if !tool_calls.is_empty() {
println!(" Tool Calls");
println!(" ----------");
println!(
" {:<6} {:<14} {:>10} {:<6}",
"Turn", "Tool", "Time (ms)", "OK?"
);
println!(" {}", "-".repeat(40));
for tc in &tool_calls {
println!(
" {:<6} {:<14} {:>10.2} {:<6}",
tc.turn,
tc.name,
tc.duration_ms,
if tc.is_error { "ERR" } else { "OK" }
);
}
println!();
let mut histogram: HashMap<String, u32> = HashMap::new();
for tc in &tool_calls {
*histogram.entry(tc.name.clone()).or_default() += 1;
}
println!(" Tool Histogram");
println!(" --------------");
for (name, count) in &histogram {
let bar = "#".repeat(*count as usize * 4);
println!(" {:<14} {:>3}x {}", name, count, bar);
}
println!();
}
println!(" Billing Estimate");
println!(" ----------------");
let input_cost_sonnet = (total_in as f64 / 1_000_000.0) * SONNET_INPUT_PER_MTOK;
let output_cost_sonnet = (total_out as f64 / 1_000_000.0) * SONNET_OUTPUT_PER_MTOK;
let input_cost_opus = (total_in as f64 / 1_000_000.0) * OPUS_INPUT_PER_MTOK;
let output_cost_opus = (total_out as f64 / 1_000_000.0) * OPUS_OUTPUT_PER_MTOK;
let input_cost_haiku = (total_in as f64 / 1_000_000.0) * HAIKU_INPUT_PER_MTOK;
let output_cost_haiku = (total_out as f64 / 1_000_000.0) * HAIKU_OUTPUT_PER_MTOK;
println!(
" This session's tokens ({} in / {} out) would cost:",
total_in, total_out
);
println!();
println!(
" {:<24} {:>10} {:>10} {:>10}",
"Model", "Input", "Output", "Total"
);
println!(" {}", "-".repeat(56));
println!(
" {:<24} ${:>9.6} ${:>9.6} ${:>9.6}",
"Claude Sonnet 4.6",
input_cost_sonnet,
output_cost_sonnet,
input_cost_sonnet + output_cost_sonnet
);
println!(
" {:<24} ${:>9.6} ${:>9.6} ${:>9.6}",
"Claude Opus 4.6",
input_cost_opus,
output_cost_opus,
input_cost_opus + output_cost_opus
);
println!(
" {:<24} ${:>9.6} ${:>9.6} ${:>9.6}",
"Claude Haiku 4.5",
input_cost_haiku,
output_cost_haiku,
input_cost_haiku + output_cost_haiku
);
println!();
println!(" Projected Costs (at Sonnet rates)");
println!(" ----------------------------------");
let session_cost = input_cost_sonnet + output_cost_sonnet;
println!(" This session: ${:.6}", session_cost);
println!(" 10 sessions/day: ${:.4}", session_cost * 10.0);
println!(" 100 sessions/day: ${:.2}", session_cost * 100.0);
println!(
" Monthly (30d, 50/day): ${:.2}",
session_cost * 50.0 * 30.0
);
println!();
println!(" Efficiency");
println!(" ----------");
let tokens_per_sec = (total_in + total_out) as f64 / elapsed.as_secs_f64();
let cost_per_turn = total_cost / turns.len().max(1) as f64;
let cost_per_tool = if !tool_calls.is_empty() {
total_cost / tool_calls.len() as f64
} else {
0.0
};
println!(" Tokens/sec: {:.0}", tokens_per_sec);
println!(" Cost/turn: ${:.6}", cost_per_turn);
println!(" Cost/tool call: ${:.6}", cost_per_tool);
println!(
" Input/output ratio: {:.1}x",
total_in as f64 / total_out.max(1) as f64
);
println!();
println!(" Verification");
println!(" ------------");
let cost_matches = (total_cost - output.usage.cost_usd.unwrap_or(0.0)).abs() < 0.000001;
let token_match =
output.usage.input_tokens == total_in && output.usage.output_tokens == total_out;
let tool_count_match = output.tool_calls.len() == tool_calls.len();
println!(
" Cost tracking: {} (reporter=${:.6} vs output=${:.6})",
if cost_matches { "PASS" } else { "FAIL" },
total_cost,
output.usage.cost_usd.unwrap_or(0.0)
);
println!(
" Token tracking: {} ({}in/{}out vs {}in/{}out)",
if token_match { "PASS" } else { "FAIL" },
total_in,
total_out,
output.usage.input_tokens,
output.usage.output_tokens
);
println!(
" Tool call count: {} ({} reporter vs {} output)",
if tool_count_match { "PASS" } else { "FAIL" },
tool_calls.len(),
output.tool_calls.len()
);
println!();
println!("{}", "=".repeat(64));
if cost_matches && token_match && tool_count_match {
println!(" All verifications PASSED");
} else {
println!(" SOME VERIFICATIONS FAILED — investigate above");
}
println!("{}", "=".repeat(64));
println!();
Ok(())
}