use {
crate::inference::models::{SUPPORTED_MODELS, SUPPORTED_MODEL_INFO},
log::{debug, info, warn},
serde::{Deserialize, Serialize},
std::{
fs,
path::{Path, PathBuf},
},
tsync::tsync,
};
#[tsync]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LocalHfModel {
pub model_id: String,
pub org: String,
pub name: String,
pub size_bytes: u64,
pub size_display: String,
pub path: String,
pub revisions: Vec<String>,
}
#[tsync]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LocalHfModelsResponse {
pub models: Vec<LocalHfModel>,
pub cache_path: String,
pub total_size_bytes: u64,
pub total_size_display: String,
}
#[tsync]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SupportedHfModel {
pub model_id: String,
pub name: String,
pub org: String,
pub description: String,
pub is_downloaded: bool,
pub is_incomplete: bool,
pub local_size_bytes: u64,
pub local_size_display: String,
pub expected_size_bytes: u64,
pub expected_size_display: String,
}
#[tsync]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SupportedHfModelsResponse {
pub models: Vec<SupportedHfModel>,
}
#[tsync]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelDownloadProgress {
pub model_id: String,
pub downloaded_bytes: u64,
pub downloaded_display: String,
pub total_bytes: u64,
pub total_display: String,
pub progress: f64,
pub done: bool,
}
const DOWNLOAD_COMPLETE_THRESHOLD: f64 = 0.99;
fn hf_cache_dir() -> Option<PathBuf> {
if let Ok(cache) = std::env::var("HUGGINGFACE_HUB_CACHE") {
let p = PathBuf::from(cache);
let _ = fs::create_dir_all(&p);
if p.is_dir() {
return Some(p);
}
}
if let Ok(hf_home) = std::env::var("HF_HOME") {
let p = PathBuf::from(hf_home).join("hub");
let _ = fs::create_dir_all(&p);
if p.is_dir() {
return Some(p);
}
}
if let Some(home) = home::home_dir() {
let p = home.join(".cache").join("huggingface").join("hub");
let _ = fs::create_dir_all(&p);
if p.is_dir() {
return Some(p);
}
}
None
}
fn dir_size(path: &PathBuf) -> u64 {
let mut total: u64 = 0;
if let Ok(entries) = fs::read_dir(path) {
for entry in entries.flatten() {
let entry_path = entry.path();
if entry_path.is_dir() {
total += dir_size(&entry_path);
} else {
if let Ok(meta) = entry_path.symlink_metadata() {
if meta.is_symlink() {
if let Ok(real_meta) = entry_path.metadata() {
total += real_meta.len();
}
} else {
total += meta.len();
}
}
}
}
}
total
}
fn format_size(bytes: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = KB * 1024;
const GB: u64 = MB * 1024;
const TB: u64 = GB * 1024;
if bytes >= TB {
format!("{:.2} TB", bytes as f64 / TB as f64)
} else if bytes >= GB {
format!("{:.2} GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.1} MB", bytes as f64 / MB as f64)
} else if bytes >= KB {
format!("{:.0} KB", bytes as f64 / KB as f64)
} else {
format!("{} B", bytes)
}
}
fn list_revisions(model_dir: &Path) -> Vec<String> {
let snapshots_dir = model_dir.join("snapshots");
if !snapshots_dir.is_dir() {
return Vec::new();
}
let mut revisions = Vec::new();
if let Ok(entries) = fs::read_dir(&snapshots_dir) {
for entry in entries.flatten() {
if entry.path().is_dir() {
if let Some(name) = entry.file_name().to_str() {
revisions.push(name.to_string());
}
}
}
}
revisions.sort();
revisions
}
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "tvos",
target_os = "visionos",
target_os = "watchos",
target_os = "android"
))]
fn make_progress(model_id: &str, downloaded: u64, total: u64, done: bool) -> ModelDownloadProgress {
let progress = if total > 0 {
(downloaded as f64 / total as f64).min(1.0)
} else {
0.0
};
ModelDownloadProgress {
model_id: model_id.to_string(),
downloaded_bytes: downloaded,
downloaded_display: format_size(downloaded),
total_bytes: total,
total_display: format_size(total),
progress,
done,
}
}
pub fn model_cache_path(model_id: &str) -> Option<PathBuf> {
let cache_dir = hf_cache_dir()?;
let dir_name = format!("models--{}", model_id.replace('/', "--"));
Some(cache_dir.join(dir_name))
}
pub fn ensure_model_cache_dir(model_id: &str) {
if let Some(model_path) = model_cache_path(model_id) {
let blobs_dir = model_path.join("blobs");
if let Err(e) = fs::create_dir_all(&blobs_dir) {
warn!(
"Could not pre-create model cache dir {}: {}",
blobs_dir.display(),
e
);
} else {
info!("Pre-created model cache dir: {}", model_path.display());
}
}
}
pub fn clean_stale_lock_files(model_id: &str) {
if let Some(cache_path) = model_cache_path(model_id) {
let blobs_dir = cache_path.join("blobs");
if blobs_dir.is_dir() {
let complete_blobs: Vec<String> = fs::read_dir(&blobs_dir)
.into_iter()
.flatten()
.flatten()
.filter_map(|e| {
let p = e.path();
if p.is_file() && p.extension().is_none() {
p.file_name().map(|n| n.to_string_lossy().to_string())
} else {
None
}
})
.collect();
if let Ok(entries) = fs::read_dir(&blobs_dir) {
for entry in entries.flatten() {
let path = entry.path();
let ext = path.extension().and_then(|e| e.to_str());
match ext {
Some("lock") => {
info!("Removing stale lock file: {}", path.display());
if let Err(e) = fs::remove_file(&path) {
warn!("Failed to remove lock file {}: {}", path.display(), e);
}
}
Some("part") => {
let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
if complete_blobs.iter().any(|b| b == stem) {
let part_size = fs::metadata(&path).map(|m| m.len()).unwrap_or(0);
info!(
"Removing stale .part file ({} bytes, complete blob exists): {}",
part_size,
path.display()
);
if let Err(e) = fs::remove_file(&path) {
warn!("Failed to remove .part file {}: {}", path.display(), e);
}
} else {
let part_size = fs::metadata(&path).map(|m| m.len()).unwrap_or(0);
debug!(
"Keeping .part file ({} bytes, no complete blob yet): {}",
part_size,
path.display()
);
}
}
_ => {}
}
}
}
}
}
}
pub fn diagnose_hf_cache(model_id: &str) {
let cache_path = match model_cache_path(model_id) {
Some(p) => p,
None => {
info!("diagnose_hf_cache({}): cache path not found", model_id);
return;
}
};
if !cache_path.is_dir() {
info!(
"diagnose_hf_cache({}): directory does not exist: {}",
model_id,
cache_path.display()
);
return;
}
info!(
"diagnose_hf_cache({}): scanning {}",
model_id,
cache_path.display()
);
let refs_dir = cache_path.join("refs");
if refs_dir.is_dir() {
if let Ok(entries) = fs::read_dir(&refs_dir) {
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
match fs::read_to_string(entry.path()) {
Ok(content) => {
let hash = content.trim();
info!(
" refs/{} → \"{}\" (len={})",
name,
if hash.len() > 12 {
format!("{}…", &hash[..12])
} else {
hash.to_string()
},
hash.len()
);
}
Err(e) => {
warn!(" refs/{} → <unreadable: {}>", name, e);
}
}
}
}
} else {
warn!(" refs/ directory missing!");
}
let blobs_dir = cache_path.join("blobs");
if blobs_dir.is_dir() {
if let Ok(entries) = fs::read_dir(&blobs_dir) {
for entry in entries.flatten() {
let path = entry.path();
let name = entry.file_name().to_string_lossy().to_string();
let size = fs::metadata(&path).map(|m| m.len()).unwrap_or(0);
let kind = if name.ends_with(".lock") {
"LOCK"
} else if name.ends_with(".part") {
"PART (incomplete)"
} else {
"blob"
};
info!(
" blobs/{} — {} ({} bytes / {:.1} MB)",
name,
kind,
size,
size as f64 / 1_048_576.0
);
}
}
} else {
warn!(" blobs/ directory missing!");
}
let snapshots_dir = cache_path.join("snapshots");
if snapshots_dir.is_dir() {
if let Ok(revs) = fs::read_dir(&snapshots_dir) {
for rev_entry in revs.flatten() {
let rev_name = rev_entry.file_name().to_string_lossy().to_string();
let rev_display = if rev_name.len() > 12 {
format!("{}…", &rev_name[..12])
} else {
rev_name.clone()
};
info!(" snapshots/{}/", rev_display);
if let Ok(files) = fs::read_dir(rev_entry.path()) {
for file_entry in files.flatten() {
let file_name = file_entry.file_name().to_string_lossy().to_string();
let file_path = file_entry.path();
if file_path.is_symlink() {
let target = fs::read_link(&file_path)
.map(|t| t.to_string_lossy().to_string())
.unwrap_or_else(|_| "<unreadable>".to_string());
let accessible = fs::metadata(&file_path).is_ok();
let status = if accessible { "OK" } else { "BROKEN" };
info!(" {} → {} [symlink: {}]", file_name, target, status);
} else if file_path.is_file() {
let size = fs::metadata(&file_path).map(|m| m.len()).unwrap_or(0);
info!(
" {} [{} bytes / {:.1} MB, real file]",
file_name,
size,
size as f64 / 1_048_576.0
);
} else {
warn!(" {} [not a file or symlink!]", file_name);
}
}
}
}
}
} else {
warn!(" snapshots/ directory missing!");
}
info!("diagnose_hf_cache({}): done", model_id);
}
pub fn repair_hf_cache_symlinks(model_id: &str) {
let cache_path = match model_cache_path(model_id) {
Some(p) if p.is_dir() => p,
_ => return,
};
let blobs_dir = cache_path.join("blobs");
let snapshots_dir = cache_path.join("snapshots");
let refs_dir = cache_path.join("refs");
if !blobs_dir.is_dir() || !snapshots_dir.is_dir() {
return;
}
let blob_names: Vec<String> = match fs::read_dir(&blobs_dir) {
Ok(entries) => entries
.flatten()
.filter_map(|e| {
let name = e.file_name().to_string_lossy().to_string();
if name.ends_with(".lock") {
None
} else if e.path().is_file() {
Some(name)
} else {
None
}
})
.collect(),
Err(_) => return,
};
if blob_names.is_empty() {
return;
}
let snapshot_revs: Vec<(String, PathBuf)> = match fs::read_dir(&snapshots_dir) {
Ok(entries) => entries
.flatten()
.filter(|e| e.path().is_dir())
.map(|e| {
let rev = e.file_name().to_string_lossy().to_string();
(rev, e.path())
})
.collect(),
Err(_) => return,
};
for (rev, snap_dir) in &snapshot_revs {
let entries = match fs::read_dir(snap_dir) {
Ok(e) => e,
Err(_) => continue,
};
for entry in entries.flatten() {
let snap_file = entry.path();
let file_name = entry.file_name().to_string_lossy().to_string();
let needs_repair = if snap_file.is_symlink() {
fs::metadata(&snap_file).is_err()
} else if snap_file.exists() {
false
} else {
true
};
if !needs_repair {
continue;
}
let blob_name = if let Ok(target) = fs::read_link(&snap_file) {
target
.file_name()
.map(|n| n.to_string_lossy().to_string())
.filter(|n| blob_names.contains(n))
} else {
None
};
let resolved_blob = if let Some(ref name) = blob_name {
Some(blobs_dir.join(name))
} else if blob_names.len() == 1 {
Some(blobs_dir.join(&blob_names[0]))
} else {
None
};
if let Some(blob_path) = resolved_blob {
if !blob_path.is_file() {
continue;
}
if snap_file.is_symlink() || snap_file.exists() {
let _ = fs::remove_file(&snap_file);
}
info!(
"repair_hf_cache_symlinks: replacing broken symlink with copy: {} → {}",
blob_path.display(),
snap_file.display()
);
if fs::hard_link(&blob_path, &snap_file).is_err() {
if let Err(e) = fs::copy(&blob_path, &snap_file) {
warn!(
"repair_hf_cache_symlinks: failed to copy blob to snapshot: {}",
e
);
}
}
} else {
warn!(
"repair_hf_cache_symlinks: broken symlink for '{}' but no matching blob found",
file_name
);
}
}
if !refs_dir.join("main").is_file() {
let _ = fs::create_dir_all(&refs_dir);
if let Err(e) = fs::write(refs_dir.join("main"), rev.as_bytes()) {
warn!("repair_hf_cache_symlinks: failed to write refs/main: {}", e);
} else {
info!("repair_hf_cache_symlinks: created refs/main → {}", rev);
}
}
}
}
pub fn list_local_hf_models() -> LocalHfModelsResponse {
let cache_dir = match hf_cache_dir() {
Some(dir) => dir,
None => {
debug!("HuggingFace cache directory not found.");
return LocalHfModelsResponse {
models: Vec::new(),
cache_path: String::new(),
total_size_bytes: 0,
total_size_display: "0 B".to_string(),
};
}
};
let cache_path_str = cache_dir.to_string_lossy().to_string();
debug!("Scanning HuggingFace cache at: {}", cache_path_str);
let mut models: Vec<LocalHfModel> = Vec::new();
let entries = match fs::read_dir(&cache_dir) {
Ok(entries) => entries,
Err(e) => {
warn!("Failed to read HuggingFace cache directory: {:?}", e);
return LocalHfModelsResponse {
models: Vec::new(),
cache_path: cache_path_str,
total_size_bytes: 0,
total_size_display: "0 B".to_string(),
};
}
};
for entry in entries.flatten() {
let entry_path = entry.path();
if !entry_path.is_dir() {
continue;
}
let dir_name = match entry.file_name().to_str() {
Some(name) => name.to_string(),
None => continue,
};
if !dir_name.starts_with("models--") {
continue;
}
let remainder = &dir_name["models--".len()..];
let parts: Vec<&str> = remainder.splitn(2, "--").collect();
if parts.len() != 2 {
let model_id = remainder.replace("--", "/");
let size = dir_size(&entry_path);
let revisions = list_revisions(&entry_path);
models.push(LocalHfModel {
model_id: model_id.clone(),
org: String::new(),
name: model_id,
size_bytes: size,
size_display: format_size(size),
path: entry_path.to_string_lossy().to_string(),
revisions,
});
continue;
}
let org = parts[0].to_string();
let name = parts[1].to_string();
let model_id = format!("{}/{}", org, name);
let size = dir_size(&entry_path);
let revisions = list_revisions(&entry_path);
models.push(LocalHfModel {
model_id,
org,
name,
size_bytes: size,
size_display: format_size(size),
path: entry_path.to_string_lossy().to_string(),
revisions,
});
}
models.retain(|m| SUPPORTED_MODELS.iter().any(|&s| s == m.model_id));
models.sort_by(|a, b| a.model_id.to_lowercase().cmp(&b.model_id.to_lowercase()));
let total_size_bytes: u64 = models.iter().map(|m| m.size_bytes).sum();
debug!(
"Found {} local HuggingFace model(s), total size: {}",
models.len(),
format_size(total_size_bytes)
);
LocalHfModelsResponse {
models,
cache_path: cache_path_str,
total_size_bytes,
total_size_display: format_size(total_size_bytes),
}
}
pub fn delete_local_hf_model(model_id: String) -> Result<(), String> {
let cache_dir = hf_cache_dir().ok_or("HuggingFace cache directory not found.".to_string())?;
let dir_name = format!("models--{}", model_id.replace('/', "--"));
let model_path = cache_dir.join(&dir_name);
if !model_path.exists() {
return Err(format!(
"Model cache directory not found: {}",
model_path.display()
));
}
if !model_path.is_dir() {
return Err(format!(
"Expected a directory but found a file: {}",
model_path.display()
));
}
debug!(
"Deleting local HuggingFace model: {} at {:?}",
model_id, model_path
);
fs::remove_dir_all(&model_path).map_err(|e| {
let msg = format!("Failed to delete model {}: {}", model_id, e);
warn!("{}", msg);
msg
})?;
debug!("Successfully deleted local model: {}", model_id);
Ok(())
}
pub fn list_supported_hf_models() -> SupportedHfModelsResponse {
let models = SUPPORTED_MODEL_INFO
.iter()
.map(|info| {
let cache_path = model_cache_path(info.id);
let local_size = cache_path
.as_ref()
.filter(|p| p.exists())
.map(dir_size)
.unwrap_or(0);
let has_cache = local_size > 0;
let is_complete = info.expected_size_bytes > 0
&& local_size as f64
>= info.expected_size_bytes as f64 * DOWNLOAD_COMPLETE_THRESHOLD;
SupportedHfModel {
model_id: info.id.to_string(),
name: info.name.to_string(),
org: info.org.to_string(),
description: info.description.to_string(),
is_downloaded: has_cache && is_complete,
is_incomplete: has_cache && !is_complete,
local_size_bytes: local_size,
local_size_display: format_size(local_size),
expected_size_bytes: info.expected_size_bytes,
expected_size_display: format_size(info.expected_size_bytes),
}
})
.collect();
SupportedHfModelsResponse { models }
}
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "tvos",
target_os = "visionos",
target_os = "watchos"
))]
pub async fn download_model<F>(
model_id: String,
on_progress: F,
app_data_dir: Option<PathBuf>,
) -> Result<(), String>
where
F: Fn(ModelDownloadProgress) + Send + Sync + Clone + 'static,
{
use crate::inference::models::{
BARTOWSKI_QWEN25_1_5B_INSTRUCT_GGUF, BARTOWSKI_QWEN25_3B_INSTRUCT_GGUF,
QWEN25_1_5B_GGUF_FILE, QWEN25_3B_GGUF_FILE,
};
use crate::inference::token::hf_token_source;
use mistralrs::GgufModelBuilder;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use std::time::Duration;
if let Some(ref data_dir) = app_data_dir {
let hf_home = data_dir.join("models");
let hf_hub_cache = hf_home.join("hub");
fs::create_dir_all(&hf_hub_cache)
.map_err(|e| format!("Cannot create HF cache dir: {e}"))?;
std::env::set_var("HF_HUB_CACHE", &hf_hub_cache);
std::env::set_var("HF_HOME", &hf_home);
info!(
"HF hub cache resolved to app data path: {}",
hf_hub_cache.display()
);
}
if !SUPPORTED_MODELS.contains(&model_id.as_str()) {
return Err(format!("Model {} is not supported.", model_id));
}
let expected_size = SUPPORTED_MODEL_INFO
.iter()
.find(|m| m.id == model_id)
.map(|m| m.expected_size_bytes)
.unwrap_or(0);
info!(
"Starting download for model: {} (expected ~{})",
model_id,
format_size(expected_size)
);
clean_stale_lock_files(&model_id);
repair_hf_cache_symlinks(&model_id);
ensure_model_cache_dir(&model_id);
let initial_bytes = model_cache_path(&model_id)
.filter(|p| p.exists())
.map(|p| dir_size(&p))
.unwrap_or(0);
on_progress(make_progress(
&model_id,
initial_bytes,
expected_size,
false,
));
let finished = Arc::new(AtomicBool::new(false));
let finished_clone = Arc::clone(&finished);
let monitor_model_id = model_id.clone();
let monitor_cb = on_progress.clone();
let monitor_handle = std::thread::spawn(move || loop {
std::thread::sleep(Duration::from_secs(2));
if finished_clone.load(Ordering::Relaxed) {
break;
}
let current_bytes = model_cache_path(&monitor_model_id)
.filter(|p| p.exists())
.map(|p| dir_size(&p))
.unwrap_or(0);
monitor_cb(make_progress(
&monitor_model_id,
current_bytes,
expected_size,
false,
));
});
let result: Result<mistralrs::Model, anyhow::Error> = match model_id.as_str() {
BARTOWSKI_QWEN25_1_5B_INSTRUCT_GGUF => {
GgufModelBuilder::new(&model_id, vec![QWEN25_1_5B_GGUF_FILE])
.with_token_source(hf_token_source())
.build()
.await
}
BARTOWSKI_QWEN25_3B_INSTRUCT_GGUF => {
GgufModelBuilder::new(&model_id, vec![QWEN25_3B_GGUF_FILE])
.with_token_source(hf_token_source())
.build()
.await
}
_ => Err(anyhow::anyhow!(
"Model '{}' is not part of the chat-only release.",
model_id
)),
};
finished.store(true, Ordering::Relaxed);
let _ = monitor_handle.join();
match result {
Ok(_model) => {
let final_bytes = model_cache_path(&model_id)
.filter(|p| p.exists())
.map(|p| dir_size(&p))
.unwrap_or(expected_size);
on_progress(make_progress(&model_id, final_bytes, final_bytes, true));
info!("Successfully downloaded model: {}", model_id);
Ok(())
}
Err(e) => {
let msg = format!("Failed to download model {}: {}", model_id, e);
warn!("{}", msg);
Err(msg)
}
}
}
#[cfg(target_os = "android")]
pub async fn download_model<F>(
model_id: String,
on_progress: F,
app_data_dir: Option<PathBuf>,
) -> Result<(), String>
where
F: Fn(ModelDownloadProgress) + Send + Sync + Clone + 'static,
{
use crate::inference::models::{
BARTOWSKI_QWEN25_1_5B_INSTRUCT_GGUF, BARTOWSKI_QWEN25_3B_INSTRUCT_GGUF,
QWEN25_1_5B_GGUF_FILE, QWEN25_1_5B_TOK_MODEL_ID, QWEN25_3B_GGUF_FILE,
QWEN25_3B_TOK_MODEL_ID,
};
use crate::inference::token::hf_token_source;
use hf_hub::Cache;
use mistralrs::GgufModelBuilder;
use mistralrs_core::GLOBAL_HF_CACHE;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use std::time::Duration;
let (gguf_file, tok_model_id) = match model_id.as_str() {
id if id == BARTOWSKI_QWEN25_1_5B_INSTRUCT_GGUF => {
(QWEN25_1_5B_GGUF_FILE, QWEN25_1_5B_TOK_MODEL_ID)
}
id if id == BARTOWSKI_QWEN25_3B_INSTRUCT_GGUF => {
(QWEN25_3B_GGUF_FILE, QWEN25_3B_TOK_MODEL_ID)
}
_ => {
return Err(format!(
"Model '{}' is not supported on Android. \
Only Qwen 2.5 1.5B or 3B (GGUF) are available.",
model_id
));
}
};
let resolved_app_data = app_data_dir
.ok_or_else(|| "app_data_dir is required on Android for HF cache resolution".to_string())?;
let hf_home = resolved_app_data.join("models");
let hf_hub_cache = hf_home.join("hub");
fs::create_dir_all(&hf_hub_cache)
.map_err(|e| format!("Failed to create HF hub cache dir: {e}"))?;
GLOBAL_HF_CACHE.get_or_init(|| Cache::new(hf_hub_cache.clone()));
std::env::set_var("HF_HUB_CACHE", &hf_hub_cache);
std::env::set_var("HF_HOME", &hf_home);
info!(
"Android HF hub cache resolved to: {}",
hf_hub_cache.display()
);
let expected_size = SUPPORTED_MODEL_INFO
.iter()
.find(|m| m.id == model_id)
.map(|m| m.expected_size_bytes)
.unwrap_or(0);
info!(
"Starting Android download for model: {} (expected ~{})",
model_id,
format_size(expected_size)
);
clean_stale_lock_files(&model_id);
ensure_model_cache_dir(&model_id);
let initial_bytes = model_cache_path(&model_id)
.filter(|p| p.exists())
.map(|p| dir_size(&p))
.unwrap_or(0);
on_progress(make_progress(
&model_id,
initial_bytes,
expected_size,
false,
));
let finished = Arc::new(AtomicBool::new(false));
let finished_clone = Arc::clone(&finished);
let monitor_model_id = model_id.clone();
let monitor_cb = on_progress.clone();
let monitor_handle = std::thread::spawn(move || loop {
std::thread::sleep(Duration::from_secs(2));
if finished_clone.load(Ordering::Relaxed) {
break;
}
let current_bytes = model_cache_path(&monitor_model_id)
.filter(|p| p.exists())
.map(|p| dir_size(&p))
.unwrap_or(0);
monitor_cb(make_progress(
&monitor_model_id,
current_bytes,
expected_size,
false,
));
});
let result = GgufModelBuilder::new(&model_id, vec![gguf_file])
.with_tok_model_id(tok_model_id)
.with_token_source(hf_token_source())
.with_logging()
.build()
.await;
finished.store(true, Ordering::Relaxed);
let _ = monitor_handle.join();
match result {
Ok(_model) => {
let final_bytes = model_cache_path(&model_id)
.filter(|p| p.exists())
.map(|p| dir_size(&p))
.unwrap_or(expected_size);
on_progress(make_progress(&model_id, final_bytes, final_bytes, true));
info!("Successfully downloaded model on Android: {}", model_id);
Ok(())
}
Err(e) => {
let msg = format!("Failed to download model {}: {}", model_id, e);
warn!("{}", msg);
Err(msg)
}
}
}
#[cfg(not(any(
target_os = "macos",
target_os = "ios",
target_os = "tvos",
target_os = "visionos",
target_os = "watchos",
target_os = "android"
)))]
pub async fn download_model<F>(
model_id: String,
_on_progress: F,
_app_data_dir: Option<PathBuf>,
) -> Result<(), String>
where
F: Fn(ModelDownloadProgress) + Send + Sync + Clone + 'static,
{
debug!(
"download_model: not supported on this platform, ignoring request for {}.",
model_id
);
Err(format!(
"Model downloads are not supported on this platform (requested: {}).",
model_id
))
}