use anyhow::{Context, Result};
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct ModelFiles {
pub subdir: String,
pub base_url: String,
pub files: Vec<String>,
}
#[allow(clippy::similar_names)] pub fn ensure_reranker_model(models_dir: &Path, spec: &ModelFiles) -> Result<PathBuf> {
let model_dir = models_dir.join(&spec.subdir);
let sentinel = spec
.files
.first()
.with_context(|| format!("reranker `{}` lists no files", spec.subdir))?;
if model_dir.join(sentinel).exists() {
return Ok(model_dir);
}
fs::create_dir_all(&model_dir)
.with_context(|| format!("failed to create reranker dir {}", model_dir.display()))?;
tracing::info!(
model = spec.subdir,
"Downloading ONNX reranker weights (may be several hundred MB)..."
);
for file_name in &spec.files {
let dest = model_dir.join(file_name);
if dest.exists() {
continue;
}
let url = format!("{}/{file_name}", spec.base_url.trim_end_matches('/'));
crate::embedding::model_manager::download_file(&url, &dest)
.with_context(|| format!("failed to download {file_name} for {}", spec.subdir))?;
}
Ok(model_dir)
}
#[cfg(test)]
mod tests {
use super::*;
fn spec() -> ModelFiles {
ModelFiles {
subdir: "Test-Reranker".to_string(),
base_url: "https://example.invalid/repo/resolve/main".to_string(),
files: vec!["model_int8.onnx".to_string(), "tokenizer.json".to_string()],
}
}
#[test]
fn sentinel_short_circuits_download() {
let tmp = tempfile::TempDir::new().unwrap();
let s = spec();
let dir = tmp.path().join(&s.subdir);
fs::create_dir_all(&dir).unwrap();
fs::write(dir.join("model_int8.onnx"), b"stub").unwrap();
let got = ensure_reranker_model(tmp.path(), &s).expect("short-circuit");
assert_eq!(got, dir);
}
#[test]
fn missing_sentinel_attempts_download_and_errors_on_bad_url() {
let tmp = tempfile::TempDir::new().unwrap();
let err = ensure_reranker_model(tmp.path(), &spec()).unwrap_err();
let msg = format!("{err:#}");
assert!(
msg.contains("model_int8.onnx") || msg.contains("Test-Reranker"),
"error should name the file/model; got: {msg}"
);
}
}