Skip to main content

orch/cli/
cost.rs

1use crate::store::{self, TaskStore};
2use std::sync::Arc;
3
4/// Show cost breakdown for a specific task.
5pub async fn show_task(id: &str) -> anyhow::Result<()> {
6    let owned = crate::cli::init_store().await?;
7    let resolved = crate::cli::task_ref::resolve(&owned, id).await?;
8    let store: Option<Arc<TaskStore>> = Some(Arc::new(owned));
9    let repo = resolved.repo.clone();
10    let id = resolved.external_id.as_str();
11
12    let usage = store::get_token_usage(&store, &repo, id).await;
13    let cost_estimate = store::get_cost_estimate(&store, &repo, id).await;
14    let model = store::opt_store_get_task(&store, &repo, id)
15        .await
16        .and_then(|t| t.model)
17        .unwrap_or_default();
18
19    if usage.total_tokens() == 0 {
20        println!("No token data available for task #{}", id);
21        println!("Run the task first to collect token usage.");
22        return Ok(());
23    }
24
25    let pricing = store::pricing_for_model(&model);
26
27    println!("Task #{}: cost breakdown", id);
28    println!(
29        "├── Input tokens:  {:>12}",
30        format_tokens(usage.input_tokens)
31    );
32    println!(
33        "├── Output tokens: {:>12}",
34        format_tokens(usage.output_tokens)
35    );
36    println!(
37        "├── Input cost:    {:>12}",
38        format!("${:.4}", cost_estimate.input_cost_usd)
39    );
40    println!(
41        "├── Output cost:   {:>12}",
42        format!("${:.4}", cost_estimate.output_cost_usd)
43    );
44    println!(
45        "└── Total cost:    {:>12}",
46        format!("${:.4}", cost_estimate.total_cost_usd)
47    );
48    println!();
49    println!(
50        "Model: {}",
51        if model.is_empty() { "unknown" } else { &model }
52    );
53    println!(
54        "Pricing: ${:.2}/${:.2} per 1M tokens",
55        pricing.input_per_million_usd, pricing.output_per_million_usd
56    );
57
58    Ok(())
59}
60
61/// Show aggregate cost summary over time periods (24h, 7d, 30d).
62pub async fn show_summary() -> anyhow::Result<()> {
63    let store = crate::cli::init_store().await?;
64
65    let summary = store.get_cost_summary().await?;
66
67    println!();
68    println!("Cost Summary");
69    println!("{}", "=".repeat(60));
70    println!();
71    println!(
72        "{:<8} {:>10} {:>10} {:>10} {:>8}",
73        "PERIOD", "INPUT", "OUTPUT", "COST", "TASKS"
74    );
75    println!("{}", "-".repeat(60));
76
77    for period in &summary.periods {
78        println!(
79            "{:<8} {:>10} {:>10} {:>10} {:>8}",
80            period.label,
81            format_tokens(period.input_tokens as u64),
82            format_tokens(period.output_tokens as u64),
83            format!("${:.4}", period.total_cost_usd),
84            period.task_count,
85        );
86    }
87    println!();
88
89    Ok(())
90}
91
92/// Show cost breakdown by agent.
93pub async fn show_by_agent() -> anyhow::Result<()> {
94    let store = crate::cli::init_store().await?;
95
96    let groups = store.get_cost_by_agent().await?;
97
98    if groups.is_empty() {
99        println!("No cost data available yet.");
100        return Ok(());
101    }
102
103    println!();
104    println!("Cost by Agent");
105    println!("{}", "=".repeat(60));
106    println!();
107    println!(
108        "{:<12} {:>10} {:>10} {:>10} {:>8}",
109        "AGENT", "INPUT", "OUTPUT", "COST", "TASKS"
110    );
111    println!("{}", "-".repeat(60));
112
113    let mut total_cost = 0.0;
114    for group in &groups {
115        total_cost += group.total_cost_usd;
116        println!(
117            "{:<12} {:>10} {:>10} {:>10} {:>8}",
118            group.name,
119            format_tokens(group.input_tokens as u64),
120            format_tokens(group.output_tokens as u64),
121            format!("${:.4}", group.total_cost_usd),
122            group.task_count,
123        );
124    }
125    println!("{}", "-".repeat(60));
126    println!(
127        "{:<12} {:>10} {:>10} {:>10}",
128        "",
129        "",
130        "",
131        format!("${:.4}", total_cost)
132    );
133    println!();
134
135    Ok(())
136}
137
138/// Show cost breakdown by model.
139pub async fn show_by_model() -> anyhow::Result<()> {
140    let store = crate::cli::init_store().await?;
141
142    let groups = store.get_cost_by_model().await?;
143
144    if groups.is_empty() {
145        println!("No cost data available yet.");
146        return Ok(());
147    }
148
149    println!();
150    println!("Cost by Model");
151    println!("{}", "=".repeat(60));
152    println!();
153    println!(
154        "{:<20} {:>10} {:>10} {:>10} {:>8}",
155        "MODEL", "INPUT", "OUTPUT", "COST", "TASKS"
156    );
157    println!("{}", "-".repeat(60));
158
159    let mut total_cost = 0.0;
160    for group in &groups {
161        total_cost += group.total_cost_usd;
162        println!(
163            "{:<20} {:>10} {:>10} {:>10} {:>8}",
164            group.name,
165            format_tokens(group.input_tokens as u64),
166            format_tokens(group.output_tokens as u64),
167            format!("${:.4}", group.total_cost_usd),
168            group.task_count,
169        );
170    }
171    println!("{}", "-".repeat(60));
172    println!(
173        "{:<20} {:>10} {:>10} {:>10}",
174        "",
175        "",
176        "",
177        format!("${:.4}", total_cost)
178    );
179    println!();
180
181    Ok(())
182}
183
184/// Format token counts with thousands separators.
185fn format_tokens(n: u64) -> String {
186    if n == 0 {
187        return "0".to_string();
188    }
189    let s = n.to_string();
190    let mut result = String::new();
191    for (i, c) in s.chars().rev().enumerate() {
192        if i > 0 && i % 3 == 0 {
193            result.push(',');
194        }
195        result.push(c);
196    }
197    result.chars().rev().collect()
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn format_tokens_zero() {
206        assert_eq!(format_tokens(0), "0");
207    }
208
209    #[test]
210    fn format_tokens_small() {
211        assert_eq!(format_tokens(123), "123");
212    }
213
214    #[test]
215    fn format_tokens_thousands() {
216        assert_eq!(format_tokens(1_234), "1,234");
217    }
218
219    #[test]
220    fn format_tokens_millions() {
221        assert_eq!(format_tokens(1_234_567), "1,234,567");
222    }
223}