leviath_cli/commands/
stages.rs1use clap::Args;
12use leviath_core::run_meta::StageRecord;
13
14#[derive(Args, Debug)]
16pub struct StagesArgs {
17 pub run_id: String,
19 #[arg(long)]
21 pub json: bool,
22 #[arg(long)]
24 pub regions: bool,
25}
26
27pub async fn execute(args: StagesArgs) -> anyhow::Result<()> {
29 let stages = crate::runstate::read_stages_index(&args.run_id);
30 if stages.is_empty() {
31 anyhow::bail!(
32 "no stage ledger for run '{}' (no readable stages.json)",
33 args.run_id
34 );
35 }
36 match args.json {
37 true => println!(
38 "{}",
39 serde_json::to_string_pretty(&stages).expect("a stage ledger serializes")
40 ),
41 false => print_ledger(&stages, args.regions),
42 }
43 Ok(())
44}
45
46fn print_ledger(stages: &[StageRecord], with_regions: bool) {
49 println!(
50 "{:<20} {:<10} {:>10} {:>10} {:>10} {:>10}",
51 "STAGE", "STATUS", "PROMPT", "OUTPUT", "CACHE RD", "CACHE WR"
52 );
53 for stage in stages {
54 println!(
55 "{:<20} {:<10} {:>10} {:>10} {:>10} {:>10}",
56 truncate(&stage.name, 20),
57 format!("{:?}", stage.status).to_lowercase(),
58 stage.prompt_tokens,
59 stage.completion_tokens,
60 stage.cached_tokens,
61 stage.cache_write_tokens,
62 );
63 if with_regions {
64 let mut regions: Vec<(&String, &usize)> = stage.region_tokens.iter().collect();
67 regions.sort_by(|a, b| b.1.cmp(a.1).then_with(|| a.0.cmp(b.0)));
68 for (name, tokens) in regions {
69 println!(" {:<18} {:>42}", truncate(name, 18), tokens);
70 }
71 }
72 }
73
74 let prompt: usize = stages.iter().map(|s| s.prompt_tokens).sum();
75 let output: usize = stages.iter().map(|s| s.completion_tokens).sum();
76 let read: usize = stages.iter().map(|s| s.cached_tokens).sum();
77 let written: usize = stages.iter().map(|s| s.cache_write_tokens).sum();
78 println!(
79 "{:<20} {:<10} {:>10} {:>10} {:>10} {:>10}",
80 "TOTAL", "", prompt, output, read, written
81 );
82}
83
84fn truncate(s: &str, width: usize) -> String {
90 match s.chars().count() > width {
91 true => s.chars().take(width).collect(),
92 false => s.to_string(),
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99 use leviath_core::run_meta::{StageRecord, StageRunStatus};
100
101 fn record(name: &str, prompt: usize) -> StageRecord {
102 let mut r = StageRecord::new(name.to_string(), 0);
103 r.prompt_tokens = prompt;
104 r.completion_tokens = 10;
105 r.cached_tokens = 5;
106 r.cache_write_tokens = 7;
107 r.status = StageRunStatus::Complete;
108 r
109 }
110
111 #[test]
112 fn the_ledger_prints_without_regions() {
113 print_ledger(&[record("ingest", 100)], false);
114 }
115
116 #[test]
117 fn the_ledger_prints_region_sizes_largest_first() {
118 let mut r = record("compute", 100);
119 r.region_tokens.insert("small".to_string(), 10);
120 r.region_tokens.insert("data_preview".to_string(), 6692);
121 r.region_tokens.insert("alpha".to_string(), 42);
125 r.region_tokens.insert("beta".to_string(), 42);
126 print_ledger(&[r], true);
127 }
128
129 #[test]
132 fn a_long_name_is_truncated_on_a_char_boundary() {
133 assert_eq!(truncate("short", 20), "short");
134 assert_eq!(truncate(&"é".repeat(30), 5).chars().count(), 5);
135 }
136
137 async fn with_ledger<R, Fut>(unique: &str, f: impl FnOnce(String) -> Fut) -> R
139 where
140 Fut: std::future::Future<Output = R>,
141 {
142 crate::runstate::with_isolated_runs_dir_async(unique, |base| async move {
143 let run_id = "run-1";
144 let dir = base.join("runs").join(run_id);
145 std::fs::create_dir_all(&dir).expect("runs dir");
146 let mut rec = record("ingest", 16_832);
147 rec.region_tokens.insert("data_preview".to_string(), 6692);
148 let json = serde_json::to_string(&[rec]).expect("serializes");
149 std::fs::write(dir.join("stages.json"), json).expect("write");
150 f(run_id.to_string()).await
151 })
152 .await
153 }
154
155 #[tokio::test]
156 async fn the_table_reads_a_real_ledger() {
157 with_ledger("stages-table", |run_id| async move {
158 execute(StagesArgs {
159 run_id,
160 json: false,
161 regions: true,
162 })
163 .await
164 .expect("a ledger on disk is readable");
165 })
166 .await;
167 }
168
169 #[tokio::test]
170 async fn the_json_form_reads_the_same_ledger() {
171 with_ledger("stages-json", |run_id| async move {
172 execute(StagesArgs {
173 run_id,
174 json: true,
175 regions: false,
176 })
177 .await
178 .expect("json is the same read, printed differently");
179 })
180 .await;
181 }
182
183 #[tokio::test]
184 async fn a_run_with_no_ledger_is_an_error_rather_than_an_empty_table() {
185 let err = execute(StagesArgs {
186 run_id: "no-such-run".to_string(),
187 json: false,
188 regions: false,
189 })
190 .await
191 .expect_err("a missing ledger is worth saying");
192 assert!(err.to_string().contains("no stage ledger"), "{err}");
193 }
194}