Skip to main content

oxibrain_cli/cmd/
model.rs

1//! `oxibrain model {list,pull,verify,use}` — model artifact management (§8.4).
2
3use anyhow::Context as _;
4use oxibrain::models::{
5    cli_progress, default_manifest, digest_file, load_manifest, model_dir, pull_entry,
6    save_manifest, verify_entry,
7};
8
9pub async fn run(args: &crate::cli::ModelCmd) -> anyhow::Result<()> {
10    let dir = model_dir();
11    match args {
12        crate::cli::ModelCmd::List => list(&dir).await,
13        crate::cli::ModelCmd::Pull { name } => pull(&dir, name.as_deref()).await,
14        crate::cli::ModelCmd::Verify { name } => verify(&dir, name.as_deref()).await,
15        crate::cli::ModelCmd::Use { name } => r#use(&dir, name).await,
16    }
17}
18
19async fn list(dir: &std::path::Path) -> anyhow::Result<()> {
20    let manifest = load_manifest().context("load manifest")?;
21    println!("models dir: {}", dir.display());
22    if manifest.is_empty() {
23        println!("(no models installed — run `oxibrain model pull`)");
24        return Ok(());
25    }
26    println!(
27        "{:<16} {:<24} {:>8} {:>6}  file",
28        "role", "name", "size", "status"
29    );
30    println!("{}", "-".repeat(70));
31    for entry in &manifest {
32        let path = dir.join(&entry.file);
33        let status = if !path.exists() {
34            "missing"
35        } else if verify_entry(entry, dir).is_ok() {
36            "ok"
37        } else {
38            "corrupt"
39        };
40        println!(
41            "{:<16} {:<24} {:>6} MiB {:>6}  {}",
42            entry.role.as_str(),
43            entry.name,
44            entry.size_mb,
45            status,
46            entry.file
47        );
48    }
49    Ok(())
50}
51
52async fn pull(dir: &std::path::Path, name: Option<&str>) -> anyhow::Result<()> {
53    let defaults = default_manifest();
54    let mut manifest = load_manifest().context("load manifest")?;
55
56    // Merge defaults into the manifest (add missing entries; keep existing digests).
57    for d in &defaults {
58        if !manifest.iter().any(|m| m.name == d.name) {
59            manifest.push(d.clone());
60        }
61    }
62
63    let targets: Vec<_> = match name {
64        Some(n) => manifest
65            .iter()
66            .filter(|m| m.name == n || m.file == n)
67            .cloned()
68            .collect(),
69        None => manifest.clone(),
70    };
71    if targets.is_empty() {
72        anyhow::bail!("no model named `{}` in the manifest", name.unwrap_or(""));
73    }
74
75    for entry in &targets {
76        let path = dir.join(&entry.file);
77        if path.exists() && verify_entry(entry, dir).is_ok() {
78            println!("{} already present and verified", entry.name);
79            continue;
80        }
81        println!("pulling {} ({} MiB)...", entry.name, entry.size_mb);
82        let entry_for_pull = entry.clone();
83        pull_entry(&entry_for_pull, dir, cli_progress)
84            .await
85            .with_context(|| format!("pull {}", entry.name))?;
86        println!("  {} verified", entry.name);
87    }
88    save_manifest(&manifest).context("save manifest")?;
89    Ok(())
90}
91
92async fn verify(dir: &std::path::Path, name: Option<&str>) -> anyhow::Result<()> {
93    let manifest = load_manifest().context("load manifest")?;
94    let targets: Vec<_> = match name {
95        Some(n) => manifest.iter().filter(|m| m.name == n).collect(),
96        None => manifest.iter().collect(),
97    };
98    if targets.is_empty() {
99        println!("(no models in manifest)");
100        return Ok(());
101    }
102    for &entry in &targets {
103        let path = dir.join(&entry.file);
104        if !path.exists() {
105            println!("{}: MISSING ({})", entry.name, path.display());
106            continue;
107        }
108        let actual = digest_file(&path)?;
109        let ok = actual == entry.digest;
110        println!(
111            "{}: {} (expected {}..., got {}...)",
112            entry.name,
113            if ok { "ok" } else { "CORRUPT" },
114            &entry.digest[..entry.digest.len().min(12)],
115            &actual[..actual.len().min(12)],
116        );
117    }
118    Ok(())
119}
120
121async fn r#use(dir: &std::path::Path, name: &str) -> anyhow::Result<()> {
122    let manifest = load_manifest().context("load manifest")?;
123    let entry = manifest
124        .iter()
125        .find(|m| m.name == name)
126        .with_context(|| format!("model `{name}` not in manifest"))?;
127    let path = dir.join(&entry.file);
128    if !path.exists() {
129        anyhow::bail!("model file not downloaded: run `oxibrain model pull {name}`");
130    }
131    verify_entry(entry, dir).context("verify")?;
132    // The digest is what matters for ExtractorId; the resolved file path
133    // is what the local adapter loads.
134    println!("using {} ({})", entry.name, entry.file);
135    println!("model path: {}", path.display());
136    println!("model digest: {}", entry.digest);
137    Ok(())
138}