Skip to main content

lean_ctx_patterns/
ollama.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Ollama CLI output compression.
4//!
5//! Handles `ollama list`/`ps` (drop the low-value ID hash column, prefix a
6//! model count) and `ollama pull`/`push` (strip download progress bars, keep
7//! the final status + layer count). Content commands (`run`, `chat`, `serve`,
8//! `show`) are left untouched — their output is the model's answer, not noise.
9
10use crate::ansi::strip_ansi;
11
12pub fn compress(cmd: &str, output: &str) -> Option<String> {
13    let trimmed = output.trim();
14    if trimmed.is_empty() {
15        return Some("ollama: ok".to_string());
16    }
17
18    if cmd.contains(" list") || cmd.contains(" ls") {
19        return Some(compress_table(trimmed, "model(s)"));
20    }
21    if cmd.contains(" ps") {
22        return Some(compress_table(trimmed, "running"));
23    }
24    if cmd.contains(" pull") || cmd.contains(" push") {
25        return Some(compress_transfer(trimmed));
26    }
27
28    // run/chat/serve/show emit model content — never compress.
29    None
30}
31
32/// Drop the `ID` column (a low-value 12-char hash) and the header row, prefix
33/// a count.
34fn compress_table(output: &str, noun: &str) -> String {
35    let lines: Vec<&str> = output.lines().filter(|l| !l.trim().is_empty()).collect();
36    if lines.len() < 2 {
37        return output.to_string();
38    }
39    let header = split_cols(lines[0]);
40    let drop = header.iter().position(|c| c.eq_ignore_ascii_case("ID"));
41
42    let mut rows: Vec<String> = Vec::new();
43    for line in &lines[1..] {
44        let mut cols = split_cols(line);
45        if let Some(i) = drop
46            && i < cols.len()
47        {
48            cols.remove(i);
49        }
50        rows.push(cols.join("  "));
51    }
52    format!("ollama: {} {}\n{}", rows.len(), noun, rows.join("\n"))
53}
54
55/// Strip per-layer progress bars from `pull`/`push`, keep the final status.
56fn compress_transfer(output: &str) -> String {
57    let mut layers = 0usize;
58    let mut success = false;
59    let mut errors: Vec<String> = Vec::new();
60
61    for raw in output.lines() {
62        let line = strip_ansi(raw);
63        let line = line.trim();
64        if line.is_empty() {
65            continue;
66        }
67        if (line.starts_with("pulling") || line.starts_with("pushing")) && line.contains('%') {
68            layers += 1;
69        } else if line == "success" {
70            success = true;
71        } else if line.contains("Error") || line.contains("error") {
72            errors.push(line.to_string());
73        }
74    }
75
76    if !errors.is_empty() {
77        return format!("ollama: FAILED\n  {}", errors.join("\n  "));
78    }
79    if success {
80        return format!("ollama: success ({layers} layers)");
81    }
82    format!("ollama: {layers} layers")
83}
84
85/// Split a table row on runs of 2+ spaces (columns may contain single spaces,
86/// e.g. "2.0 GB" or "3 days ago").
87fn split_cols(line: &str) -> Vec<String> {
88    let mut cols = Vec::new();
89    let mut cur = String::new();
90    let mut spaces = 0;
91    for ch in line.trim().chars() {
92        if ch == ' ' {
93            spaces += 1;
94            continue;
95        }
96        if spaces >= 2 && !cur.is_empty() {
97            cols.push(std::mem::take(&mut cur));
98        } else if spaces == 1 && !cur.is_empty() {
99            cur.push(' ');
100        }
101        spaces = 0;
102        cur.push(ch);
103    }
104    if !cur.is_empty() {
105        cols.push(cur);
106    }
107    cols
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    const LIST: &str = "NAME                    ID              SIZE      MODIFIED\nllama3.2:latest         a80c4f17acd5    2.0 GB    3 days ago\nqwen2.5-coder:7b        2b0496514337    4.7 GB    2 weeks ago\n";
115
116    #[test]
117    fn list_drops_id_keeps_name_size() {
118        let r = compress("ollama list", LIST).unwrap();
119        assert!(r.contains("2 model(s)"), "{r}");
120        assert!(r.contains("llama3.2:latest"), "{r}");
121        assert!(r.contains("2.0 GB"), "{r}");
122        assert!(r.contains("3 days ago"), "keeps multi-word column: {r}");
123        assert!(!r.contains("a80c4f17acd5"), "drops ID hash: {r}");
124    }
125
126    #[test]
127    fn pull_collapses_progress() {
128        let out = "pulling manifest\npulling aabbccdd... 100% ▕████████▏ 2.0 GB\npulling 1234efgh... 100% ▕██▏ 1.2 KB\nverifying sha256 digest\nwriting manifest\nsuccess\n";
129        let r = compress("ollama pull llama3.2", out).unwrap();
130        assert_eq!(r, "ollama: success (2 layers)");
131    }
132
133    #[test]
134    fn run_is_not_compressed() {
135        assert!(
136            compress(
137                "ollama run llama3.2 'hi'",
138                "The capital of France is Paris."
139            )
140            .is_none()
141        );
142    }
143
144    #[test]
145    fn empty_is_ok() {
146        assert_eq!(compress("ollama list", "").unwrap(), "ollama: ok");
147    }
148}