Skip to main content

ferrox_models/
hf_pull.rs

1//! Hugging Face Hub download helper (`hf download` when the CLI is installed).
2
3use std::path::{Path, PathBuf};
4use std::process::Command;
5
6/// True when `path` looks like a Hub repo id rather than a local file.
7pub fn looks_like_hf_repo(path: &str) -> bool {
8    let p = Path::new(path);
9    path.contains('/')
10        && !path.contains('\\')
11        && !p.exists()
12        && !path.ends_with(".gguf")
13        && !path.starts_with('.')
14}
15
16fn hf_available() -> bool {
17    Command::new("hf")
18        .arg("--version")
19        .output()
20        .map(|o| o.status.success())
21        .unwrap_or(false)
22}
23
24fn default_cache_dir(repo: &str) -> PathBuf {
25    std::env::var_os("HOME")
26        .map(PathBuf::from)
27        .unwrap_or_else(|| PathBuf::from("."))
28        .join(".cache/ferrox/hf")
29        .join(repo.replace('/', "--"))
30}
31
32fn resolve_gguf_in_dir(dir: &Path) -> anyhow::Result<PathBuf> {
33    let mut ggufs: Vec<PathBuf> = std::fs::read_dir(dir)?
34        .filter_map(|e| e.ok())
35        .map(|e| e.path())
36        .filter(|p| p.extension().is_some_and(|x| x == "gguf"))
37        .collect();
38    ggufs.sort();
39    ggufs.into_iter().next().ok_or_else(|| {
40        anyhow::anyhow!(
41            "no .gguf file found under {} after hf download",
42            dir.display()
43        )
44    })
45}
46
47/// Download `repo` via `hf download` and return a local `.gguf` path.
48pub fn pull_hf_gguf(
49    repo: &str,
50    file_pattern: &str,
51    local_dir: Option<PathBuf>,
52) -> anyhow::Result<PathBuf> {
53    if !hf_available() {
54        anyhow::bail!(
55            "Hugging Face CLI `hf` not found on PATH. Install: pip install huggingface_hub && hf auth login"
56        );
57    }
58
59    let local_dir = local_dir.unwrap_or_else(|| default_cache_dir(repo));
60    std::fs::create_dir_all(&local_dir)?;
61
62    let status = Command::new("hf")
63        .arg("download")
64        .arg(repo)
65        .arg(file_pattern)
66        .arg("--local-dir")
67        .arg(&local_dir)
68        .status()?;
69
70    if !status.success() {
71        anyhow::bail!("hf download failed for {repo}");
72    }
73
74    resolve_gguf_in_dir(&local_dir)
75}
76
77/// If `model` is a Hub repo id, download and return the local GGUF path.
78pub fn resolve_model_path(model: &str) -> anyhow::Result<String> {
79    if !looks_like_hf_repo(model) {
80        return Ok(model.to_string());
81    }
82    let path = pull_hf_gguf(model, "*.gguf", None)?;
83    Ok(path.display().to_string())
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn hf_repo_heuristic() {
92        assert!(looks_like_hf_repo("org/model"));
93        assert!(!looks_like_hf_repo("./local/model.gguf"));
94        assert!(!looks_like_hf_repo("model.gguf"));
95    }
96}