use std::path::{Path, PathBuf};
use crate::error::LlamaError;
use crate::hf::downloader::HfDownloader;
use crate::hf::repo::HfRepo;
pub(crate) enum ModelSource {
Local(PathBuf),
Hf {
repo: HfRepo,
filename: Option<String>,
},
}
pub(crate) fn resolve(
model_path: &Path,
hf_filename: Option<&str>,
hf_repo_override: Option<&HfRepo>,
downloader: &dyn HfDownloader,
) -> Result<PathBuf, LlamaError> {
if let Some(repo) = hf_repo_override {
return resolve_hf(repo, hf_filename, downloader);
}
let is_hf_candidate = model_path.to_str().is_some_and(HfRepo::looks_like_repo_id);
if is_hf_candidate && !model_path.exists() {
let path_str = model_path.to_str().expect("UTF-8 checked above");
let repo =
HfRepo::new(path_str).expect("looks_like_repo_id returned true but new() failed");
return resolve_hf(&repo, hf_filename, downloader);
}
Ok(model_path.to_path_buf())
}
fn resolve_hf(
repo: &HfRepo,
filename: Option<&str>,
downloader: &dyn HfDownloader,
) -> Result<PathBuf, LlamaError> {
let file = match filename {
Some(f) => f.to_string(),
None => auto_pick(repo, downloader)?,
};
downloader.get(repo, &file)
}
fn auto_pick(repo: &HfRepo, downloader: &dyn HfDownloader) -> Result<String, LlamaError> {
let files = downloader.list_repo_files(repo)?;
let gguf: Vec<String> = files.into_iter().filter(|f| f.ends_with(".gguf")).collect();
match gguf.len() {
0 => Err(LlamaError::ModelDownload(format!(
"no .gguf files in repo {}",
repo.as_str()
))),
1 => {
let file = gguf.into_iter().next().expect("len == 1");
tracing::info!(
repo = repo.as_str(),
file = %file,
"auto-picked single .gguf"
);
Ok(file)
}
n => Err(LlamaError::ModelDownload(format!(
"ambiguous: {n} gguf files in repo {}, use with_hf_filename",
repo.as_str()
))),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hf::downloader::MockHfDownloader;
use std::path::PathBuf;
#[test]
fn resolve_local_when_path_exists() {
let tmp = tempfile::NamedTempFile::new().expect("create temp file");
let path = tmp.path().to_path_buf();
let dl = MockHfDownloader::default();
let result = resolve(&path, None, None, &dl).expect("local resolves");
assert_eq!(result, path, "local path returned unchanged");
}
#[test]
fn resolve_local_when_path_looks_like_repo_id_but_file_exists() {
let tmp_dir = tempfile::tempdir().expect("create tempdir");
let path = tmp_dir.path().join("TheBloke").join("Llama");
std::fs::create_dir_all(path.parent().unwrap()).expect("mkdir");
std::fs::write(&path, b"local model bytes").expect("write");
let dl = MockHfDownloader::default();
let result = resolve(&path, None, None, &dl).expect("local resolves");
assert_eq!(result, path, "existing file wins over looks_like_repo_id");
}
#[test]
fn resolve_hf_when_repo_override_and_filename() {
let repo = HfRepo::new("TheBloke/Foo").expect("valid repo id");
let expected = PathBuf::from("/tmp/expected-explicit.gguf");
let dl = MockHfDownloader::default().with_paths(
"TheBloke/Foo",
"explicit.gguf",
expected.clone(),
);
let model_path = PathBuf::from("/nonexistent/local/model.gguf");
let result = resolve(&model_path, Some("explicit.gguf"), Some(&repo), &dl)
.expect("hf override resolves");
assert_eq!(
result, expected,
"downloader.get called with explicit filename, returned cached path"
);
}
#[test]
fn resolve_hf_when_path_looks_like_repo_id() {
let expected = PathBuf::from("/tmp/only.gguf");
let dl = MockHfDownloader::default()
.with_files("TheBloke/Foo", vec!["only.gguf".to_string()])
.with_paths("TheBloke/Foo", "only.gguf", expected.clone());
let model_path = PathBuf::from("TheBloke/Foo");
assert!(
!model_path.exists(),
"test fixture leaked: {} exists on disk",
model_path.display()
);
let result = resolve(&model_path, None, None, &dl).expect("hf auto-detect");
assert_eq!(result, expected, "auto-picked the sole .gguf");
}
#[test]
fn resolve_hf_auto_pick_1_gguf() {
let expected = PathBuf::from("/tmp/only.gguf");
let dl = MockHfDownloader::default()
.with_files("TheBloke/Foo", vec!["only.gguf".to_string()])
.with_paths("TheBloke/Foo", "only.gguf", expected.clone());
let model_path = PathBuf::from("TheBloke/Foo");
let result = resolve(&model_path, None, None, &dl).expect("hf auto-pick 1");
assert_eq!(result, expected, "1 .gguf -> picked exactly that file");
}
#[test]
fn resolve_hf_error_0_gguf() {
let dl = MockHfDownloader::default();
let model_path = PathBuf::from("TheBloke/Empty");
let err = resolve(&model_path, None, None, &dl).expect_err("0 gguf files must error");
match err {
LlamaError::ModelDownload(msg) => {
assert!(
msg.contains("no .gguf files in repo"),
"msg must say 'no .gguf files in repo', got: {msg}"
);
assert!(
msg.contains("TheBloke/Empty"),
"msg must include the repo id, got: {msg}"
);
}
other => panic!("expected ModelDownload, got {other:?}"),
}
}
#[test]
fn resolve_hf_error_many_gguf() {
let dl = MockHfDownloader::default().with_files(
"TheBloke/Ambiguous",
vec!["a.gguf".to_string(), "b.gguf".to_string()],
);
let model_path = PathBuf::from("TheBloke/Ambiguous");
let err = resolve(&model_path, None, None, &dl).expect_err(">1 gguf files must error");
match err {
LlamaError::ModelDownload(msg) => {
assert!(
msg.contains("ambiguous"),
"msg must say 'ambiguous', got: {msg}"
);
assert!(
msg.contains("2 gguf files in repo"),
"msg must include the count '2 gguf files in repo', got: {msg}"
);
assert!(
msg.contains("use with_hf_filename"),
"msg must point at the fix, got: {msg}"
);
}
other => panic!("expected ModelDownload, got {other:?}"),
}
}
#[test]
fn resolve_hf_auto_pick_filters_to_gguf() {
let expected = PathBuf::from("/tmp/a.gguf");
let dl = MockHfDownloader::default()
.with_files(
"TheBloke/Foo",
vec![
"a.gguf".to_string(),
"README.md".to_string(),
"config.json".to_string(),
],
)
.with_paths("TheBloke/Foo", "a.gguf", expected.clone());
let model_path = PathBuf::from("TheBloke/Foo");
let result = resolve(&model_path, None, None, &dl).expect("hf auto-pick filters");
assert_eq!(
result, expected,
"first .gguf wins, non-.gguf files are ignored"
);
}
}