Skip to main content

ferrox_models/
hf_pull.rs

1//! Hugging Face Hub download helper.
2//!
3//! This used to shell out to the `hf` CLI, which meant a Rust engine
4//! could not fetch its own weights without a Python install and
5//! `pip install huggingface_hub`. The transport is native now: see
6//! [`crate::hub`], which resolves IPv4 first, reads `HF_TOKEN`, honours
7//! `HF_ENDPOINT`, and asks for a byte range so an interrupted download
8//! resumes rather than starting from zero.
9//!
10//! `hf` is still used when it is on PATH and the native path is not
11//! compiled in, so an existing setup keeps working.
12
13use std::path::{Path, PathBuf};
14#[cfg(not(feature = "hub"))]
15use std::process::Command;
16
17/// True when `path` looks like a Hub repo id rather than a local file.
18pub fn looks_like_hf_repo(path: &str) -> bool {
19    let p = Path::new(path);
20    path.contains('/')
21        && !path.contains('\\')
22        && !p.exists()
23        && !path.ends_with(".gguf")
24        && !path.starts_with('.')
25}
26
27#[cfg(not(feature = "hub"))]
28fn hf_available() -> bool {
29    Command::new("hf")
30        .arg("--version")
31        .output()
32        .map(|o| o.status.success())
33        .unwrap_or(false)
34}
35
36fn default_cache_dir(repo: &str) -> PathBuf {
37    std::env::var_os("HOME")
38        .map(PathBuf::from)
39        .unwrap_or_else(|| PathBuf::from("."))
40        .join(".cache/ferrox/hf")
41        .join(repo.replace('/', "--"))
42}
43
44fn resolve_gguf_in_dir(dir: &Path) -> anyhow::Result<PathBuf> {
45    let mut ggufs: Vec<PathBuf> = std::fs::read_dir(dir)?
46        .filter_map(|e| e.ok())
47        .map(|e| e.path())
48        .filter(|p| p.extension().is_some_and(|x| x == "gguf"))
49        .collect();
50    ggufs.sort();
51    ggufs.into_iter().next().ok_or_else(|| {
52        anyhow::anyhow!(
53            "no .gguf file found under {} after hf download",
54            dir.display()
55        )
56    })
57}
58
59/// Download `repo` and return a local `.gguf` path.
60pub fn pull_hf_gguf(
61    repo: &str,
62    file_pattern: &str,
63    local_dir: Option<PathBuf>,
64) -> anyhow::Result<PathBuf> {
65    let local_dir = local_dir.unwrap_or_else(|| default_cache_dir(repo));
66    std::fs::create_dir_all(&local_dir)?;
67
68    #[cfg(feature = "hub")]
69    {
70        crate::hub::fetch_to_dir(repo, file_pattern, &local_dir)
71            .map_err(|e| anyhow::anyhow!("{e}"))?;
72        resolve_gguf_in_dir(&local_dir)
73    }
74
75    #[cfg(not(feature = "hub"))]
76    {
77        if !hf_available() {
78            anyhow::bail!(
79                "this build has no native downloader (feature `hub` is off) and the \
80                 Hugging Face CLI `hf` is not on PATH. Either install it with \
81                 `pip install huggingface_hub`, or use a ferrox build with `hub` on."
82            );
83        }
84        let status = Command::new("hf")
85            .arg("download")
86            .arg(repo)
87            .arg(file_pattern)
88            .arg("--local-dir")
89            .arg(&local_dir)
90            .status()?;
91        if !status.success() {
92            anyhow::bail!("hf download failed for {repo}");
93        }
94        resolve_gguf_in_dir(&local_dir)
95    }
96}
97
98/// If `model` is a Hub repo id, download and return the local GGUF path.
99pub fn resolve_model_path(model: &str) -> anyhow::Result<String> {
100    if !looks_like_hf_repo(model) {
101        return Ok(model.to_string());
102    }
103    let path = pull_hf_gguf(model, "*.gguf", None)?;
104    Ok(path.display().to_string())
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn hf_repo_heuristic() {
113        assert!(looks_like_hf_repo("org/model"));
114        assert!(!looks_like_hf_repo("./local/model.gguf"));
115        assert!(!looks_like_hf_repo("model.gguf"));
116    }
117}