use std::env;
use std::path::{Path, PathBuf};
use hf_hub::Cache;
use modelexpress_client::{
Client as MxClient, ClientConfig as MxClientConfig, ModelProvider as MxModelProvider,
};
use modelexpress_common::download as mx;
use dynamo_runtime::config::environment_names::model as env_model;
mod huggingface;
pub(crate) use huggingface::{
HfRepoSpec, cached_hf_snapshot, download_hf_snapshot, finalize_hf_snapshot, huggingface_cache,
};
fn get_cached_model_path(model_name: &str, ignore_weights: bool) -> Option<PathBuf> {
get_cached_model_path_in(model_name, ignore_weights, get_model_express_cache_dir())
}
fn get_cached_model_path_in(
model_name: &str,
ignore_weights: bool,
cache_dir: PathBuf,
) -> Option<PathBuf> {
let cache = Cache::new(cache_dir);
let repo = cache.model(model_name.to_string());
let config_path = repo.get("config.json")?;
let has_tokenizer = repo.get("tokenizer.json").is_some()
|| repo.get("tiktoken.model").is_some()
|| has_tiktoken_file(config_path.parent()?);
if !has_tokenizer {
return None;
}
if !ignore_weights {
let has_weights = repo.get("model.safetensors").is_some()
|| repo.get("pytorch_model.bin").is_some()
|| repo
.get("model.safetensors.index.json")
.is_some_and(|p| shard_files_present(&p))
|| repo
.get("pytorch_model.bin.index.json")
.is_some_and(|p| shard_files_present(&p));
if !has_weights {
return None;
}
}
let snapshot_path = config_path.parent()?.to_path_buf();
tracing::info!("Found cached model '{model_name}' at {snapshot_path:?}, skipping download");
Some(snapshot_path)
}
fn has_tiktoken_file(dir: &Path) -> bool {
std::fs::read_dir(dir)
.into_iter()
.flatten()
.flatten()
.any(|e| e.path().extension().is_some_and(|ext| ext == "tiktoken"))
}
fn shard_files_present(index_path: &Path) -> bool {
let Some(snapshot_dir) = index_path.parent() else {
return false;
};
let Ok(contents) = std::fs::read_to_string(index_path) else {
return false;
};
let Ok(value) = serde_json::from_str::<serde_json::Value>(&contents) else {
return false;
};
let Some(weight_map) = value.get("weight_map").and_then(|v| v.as_object()) else {
return false;
};
let shards: std::collections::HashSet<&str> =
weight_map.values().filter_map(|v| v.as_str()).collect();
if shards.is_empty() {
return false;
}
shards.iter().all(|s| snapshot_dir.join(s).exists())
}
fn is_offline_mode() -> bool {
dynamo_runtime::config::env_is_truthy(env_model::huggingface::HF_HUB_OFFLINE)
}
fn is_no_shared_storage() -> bool {
env::var(env_model::model_express::MODEL_EXPRESS_NO_SHARED_STORAGE)
.map(|v| v == "1" || v.to_lowercase() == "true")
.unwrap_or(false)
}
pub async fn from_hf(name: impl AsRef<Path>, ignore_weights: bool) -> anyhow::Result<PathBuf> {
let name = name.as_ref();
let model_name = name.display().to_string();
if let Some(cached_path) = get_cached_model_path(&model_name, ignore_weights) {
return Ok(cached_path);
}
if is_offline_mode() {
tracing::warn!(
"Offline mode enabled but model '{model_name}' not found in cache, attempting download anyway"
);
}
let mut config: MxClientConfig = MxClientConfig::default();
if let Ok(endpoint) = env::var(env_model::model_express::MODEL_EXPRESS_URL) {
config = config.with_endpoint(endpoint);
}
if is_no_shared_storage() {
config.cache.shared_storage = false;
}
let result = match MxClient::new(config).await {
Ok(mut client) => {
tracing::info!("Successfully connected to ModelExpress server");
match client
.request_model_with_provider_and_fallback(
&model_name,
MxModelProvider::HuggingFace,
ignore_weights,
)
.await
{
Ok(()) => {
tracing::info!("Server download succeeded for model: {model_name}");
match client
.get_model_path(&model_name, MxModelProvider::HuggingFace)
.await
{
Ok(path) => Ok(path),
Err(e) => {
tracing::warn!(
"Failed to resolve local model path after server download for '{model_name}': {e}. \
Falling back to direct download."
);
mx_download_direct(&model_name, ignore_weights).await
}
}
}
Err(e) => {
tracing::warn!(
"Server download failed for model '{model_name}': {e}. Falling back to direct download."
);
mx_download_direct(&model_name, ignore_weights).await
}
}
}
Err(e) => {
tracing::warn!("Cannot connect to ModelExpress server: {e}. Using direct download.");
mx_download_direct(&model_name, ignore_weights).await
}
};
match result {
Ok(path) => {
tracing::info!("ModelExpress download completed successfully for model: {model_name}");
Ok(path)
}
Err(e) => {
tracing::warn!("ModelExpress download failed for model '{model_name}': {e}");
Err(e)
}
}
}
async fn mx_download_direct(model_name: &str, ignore_weights: bool) -> anyhow::Result<PathBuf> {
let cache_dir = get_model_express_cache_dir();
mx::download_model(
model_name,
MxModelProvider::HuggingFace,
Some(cache_dir),
ignore_weights,
)
.await
}
fn get_model_express_cache_dir() -> PathBuf {
cache_dir_from_values(
env::var(env_model::huggingface::HF_HUB_CACHE).ok(),
env::var(env_model::huggingface::HF_HOME).ok(),
env::var(env_model::model_express::MODEL_EXPRESS_CACHE_PATH).ok(),
env::var("HOME").ok(),
env::var("USERPROFILE").ok(),
)
}
fn cache_dir_from_values(
hf_hub_cache: Option<String>,
hf_home: Option<String>,
model_express_cache: Option<String>,
home: Option<String>,
userprofile: Option<String>,
) -> PathBuf {
if let Some(cache_path) = hf_hub_cache {
return PathBuf::from(cache_path);
}
if let Some(hf_home) = hf_home {
return PathBuf::from(hf_home).join("hub");
}
if let Some(cache_path) = model_express_cache {
return PathBuf::from(cache_path);
}
PathBuf::from(home.or(userprofile).unwrap_or_else(|| ".".to_string()))
.join(".cache/huggingface/hub")
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[serial_test::serial]
#[test]
fn hf_offline_mode_accepts_huggingface_truthy_values() {
for value in ["1", "true", "TRUE", "on", "ON", "yes", "YES"] {
temp_env::with_var(env_model::huggingface::HF_HUB_OFFLINE, Some(value), || {
assert!(is_offline_mode(), "rejected {value}")
});
}
}
#[test]
fn cache_dir_precedence_and_fallback() {
assert_eq!(
cache_dir_from_values(
Some("/hub-cache".to_string()),
Some("/hf-home".to_string()),
Some("/model-express".to_string()),
Some("/home".to_string()),
None,
),
PathBuf::from("/hub-cache")
);
assert_eq!(
cache_dir_from_values(
None,
Some("/hf-home".to_string()),
Some("/model-express".to_string()),
Some("/home".to_string()),
None,
),
PathBuf::from("/hf-home/hub")
);
assert_eq!(
cache_dir_from_values(
None,
None,
Some("/model-express".to_string()),
Some("/home".to_string()),
None,
),
PathBuf::from("/model-express")
);
assert_eq!(
cache_dir_from_values(None, None, None, None, Some("/profile".to_string())),
PathBuf::from("/profile/.cache/huggingface/hub")
);
}
fn build_hf_cache(cache_root: &Path, model_name: &str, files: &[&str]) -> PathBuf {
let repo_dir = cache_root.join(format!("models--{}", model_name.replace('/', "--")));
let snapshot_hash = "0000000000000000000000000000000000000000";
let snapshot_dir = repo_dir.join("snapshots").join(snapshot_hash);
let refs_dir = repo_dir.join("refs");
fs::create_dir_all(&snapshot_dir).unwrap();
fs::create_dir_all(&refs_dir).unwrap();
fs::write(refs_dir.join("main"), snapshot_hash).unwrap();
for f in files {
fs::write(snapshot_dir.join(f), "{}").unwrap();
}
snapshot_dir
}
#[test]
fn test_cached_path_metadata_only_satisfies_ignore_weights_true() {
let temp = TempDir::new().unwrap();
let model = "test-org/metadata-only";
let snapshot = build_hf_cache(temp.path(), model, &["config.json", "tokenizer.json"]);
let with_weights = get_cached_model_path_in(model, false, temp.path().to_path_buf());
let no_weights = get_cached_model_path_in(model, true, temp.path().to_path_buf());
assert!(
with_weights.is_none(),
"metadata-only cache must NOT satisfy ignore_weights=false"
);
assert_eq!(
no_weights.as_deref(),
Some(snapshot.as_path()),
"metadata-only cache must satisfy ignore_weights=true"
);
}
#[test]
fn test_cached_path_full_cache_satisfies_both_modes() {
let temp = TempDir::new().unwrap();
let model = "test-org/full-cache";
let snapshot = build_hf_cache(
temp.path(),
model,
&["config.json", "tokenizer.json", "model.safetensors"],
);
let with_weights = get_cached_model_path_in(model, false, temp.path().to_path_buf());
let no_weights = get_cached_model_path_in(model, true, temp.path().to_path_buf());
assert_eq!(with_weights.as_deref(), Some(snapshot.as_path()));
assert_eq!(no_weights.as_deref(), Some(snapshot.as_path()));
}
#[test]
fn test_cached_path_sharded_requires_all_shard_files() {
let temp = TempDir::new().unwrap();
let model = "test-org/sharded";
let snapshot = build_hf_cache(temp.path(), model, &["config.json", "tokenizer.json"]);
fs::write(
snapshot.join("model.safetensors.index.json"),
r#"{"weight_map": {"a.weight": "model-00001-of-00002.safetensors", "b.weight": "model-00002-of-00002.safetensors"}}"#,
)
.unwrap();
let incomplete = get_cached_model_path_in(model, false, temp.path().to_path_buf());
assert!(
incomplete.is_none(),
"sharded cache without shard files must NOT satisfy ignore_weights=false"
);
fs::write(snapshot.join("model-00001-of-00002.safetensors"), "").unwrap();
fs::write(snapshot.join("model-00002-of-00002.safetensors"), "").unwrap();
let complete = get_cached_model_path_in(model, false, temp.path().to_path_buf());
assert_eq!(complete.as_deref(), Some(snapshot.as_path()));
}
#[test]
fn test_cached_path_rejects_tokenizer_config_without_real_tokenizer() {
let temp = TempDir::new().unwrap();
let model = "test-org/tokenizer-config-only";
build_hf_cache(
temp.path(),
model,
&["config.json", "tokenizer_config.json"],
);
assert!(
get_cached_model_path_in(model, true, temp.path().to_path_buf()).is_none(),
"tokenizer_config.json alone must NOT satisfy ignore_weights=true",
);
assert!(
get_cached_model_path_in(model, false, temp.path().to_path_buf()).is_none(),
"tokenizer_config.json alone must NOT satisfy ignore_weights=false",
);
}
#[serial_test::serial]
#[tokio::test]
async fn test_from_hf_cache_first_in_online_mode() {
let temp = TempDir::new().unwrap();
let model = "test-org/cache-first-online";
let snapshot = build_hf_cache(
temp.path(),
model,
&["config.json", "tokenizer.json", "model.safetensors"],
);
temp_env::async_with_vars(
[
(
env_model::huggingface::HF_HUB_CACHE,
Some(temp.path().to_str().unwrap()),
),
(env_model::huggingface::HF_HUB_OFFLINE, None),
(env_model::huggingface::HF_HOME, None),
(env_model::model_express::MODEL_EXPRESS_CACHE_PATH, None),
],
async {
let result = from_hf(PathBuf::from(model), false).await;
assert_eq!(
result.ok().as_deref(),
Some(snapshot.as_path()),
"from_hf must return cached path in online mode without network"
);
},
)
.await;
}
}