1use std::path::{Path, PathBuf};
14#[cfg(not(feature = "hub"))]
15use std::process::Command;
16
17pub 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
59pub 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
98pub 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}