Skip to main content

orch/cli/
cost.rs

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