Skip to main content

orch/cli/
cost.rs

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