//! MCP token-usage reporting tool (`tool_get_token_usage`). use serde_json::{Value, json}; use crate::http::context::AppContext; pub(crate) fn tool_get_token_usage(args: &Value, ctx: &AppContext) -> Result { let root = ctx.state.get_project_root()?; let filter_story = args.get("story_id").and_then(|v| v.as_str()); let all_records = crate::agents::token_usage::read_all(&root)?; let records: Vec<_> = all_records .into_iter() .filter(|r| filter_story.is_none_or(|s| r.story_id == s)) .collect(); let total_cost: f64 = records.iter().map(|r| r.usage.total_cost_usd).sum(); let total_input: u64 = records.iter().map(|r| r.usage.input_tokens).sum(); let total_output: u64 = records.iter().map(|r| r.usage.output_tokens).sum(); let total_cache_create: u64 = records .iter() .map(|r| r.usage.cache_creation_input_tokens) .sum(); let total_cache_read: u64 = records .iter() .map(|r| r.usage.cache_read_input_tokens) .sum(); serde_json::to_string_pretty(&json!({ "records": records.iter().map(|r| json!({ "story_id": r.story_id, "agent_name": r.agent_name, "timestamp": r.timestamp, "input_tokens": r.usage.input_tokens, "output_tokens": r.usage.output_tokens, "cache_creation_input_tokens": r.usage.cache_creation_input_tokens, "cache_read_input_tokens": r.usage.cache_read_input_tokens, "total_cost_usd": r.usage.total_cost_usd, })).collect::>(), "totals": { "records": records.len(), "input_tokens": total_input, "output_tokens": total_output, "cache_creation_input_tokens": total_cache_create, "cache_read_input_tokens": total_cache_read, "total_cost_usd": total_cost, } })) .map_err(|e| format!("Serialization error: {e}")) } #[cfg(test)] mod tests { use super::*; use crate::http::test_helpers::test_ctx; #[test] fn tool_get_token_usage_empty_returns_zero_totals() { let tmp = tempfile::tempdir().unwrap(); let ctx = test_ctx(tmp.path()); let result = tool_get_token_usage(&json!({}), &ctx).unwrap(); let parsed: Value = serde_json::from_str(&result).unwrap(); assert_eq!(parsed["records"].as_array().unwrap().len(), 0); assert_eq!(parsed["totals"]["records"], 0); assert_eq!(parsed["totals"]["total_cost_usd"], 0.0); } #[test] fn tool_get_token_usage_returns_written_records() { let tmp = tempfile::tempdir().unwrap(); let root = tmp.path(); let ctx = test_ctx(root); let usage = crate::agents::TokenUsage { input_tokens: 100, output_tokens: 200, cache_creation_input_tokens: 5000, cache_read_input_tokens: 10000, total_cost_usd: 1.57, }; let record = crate::agents::token_usage::build_record("42_story_foo", "coder-1", None, usage); crate::agents::token_usage::append_record(root, &record).unwrap(); let result = tool_get_token_usage(&json!({}), &ctx).unwrap(); let parsed: Value = serde_json::from_str(&result).unwrap(); assert_eq!(parsed["records"].as_array().unwrap().len(), 1); assert_eq!(parsed["records"][0]["story_id"], "42_story_foo"); assert_eq!(parsed["records"][0]["agent_name"], "coder-1"); assert_eq!(parsed["records"][0]["input_tokens"], 100); assert_eq!(parsed["totals"]["records"], 1); assert!((parsed["totals"]["total_cost_usd"].as_f64().unwrap() - 1.57).abs() < f64::EPSILON); } #[test] fn tool_get_token_usage_filters_by_story_id() { let tmp = tempfile::tempdir().unwrap(); let root = tmp.path(); let ctx = test_ctx(root); let usage = crate::agents::TokenUsage { input_tokens: 50, output_tokens: 60, cache_creation_input_tokens: 0, cache_read_input_tokens: 0, total_cost_usd: 0.5, }; let r1 = crate::agents::token_usage::build_record("10_story_a", "coder-1", None, usage.clone()); let r2 = crate::agents::token_usage::build_record("20_story_b", "coder-2", None, usage); crate::agents::token_usage::append_record(root, &r1).unwrap(); crate::agents::token_usage::append_record(root, &r2).unwrap(); let result = tool_get_token_usage(&json!({"story_id": "10_story_a"}), &ctx).unwrap(); let parsed: Value = serde_json::from_str(&result).unwrap(); assert_eq!(parsed["records"].as_array().unwrap().len(), 1); assert_eq!(parsed["records"][0]["story_id"], "10_story_a"); assert_eq!(parsed["totals"]["records"], 1); } }