use smbcloud_gresiq_sdk::OndeModel;
use std::{
collections::{HashMap, HashSet},
path::PathBuf,
};
#[derive(Debug, Clone)]
pub struct LocalModel {
pub model_id: String,
pub display_name: String,
pub size_display: String,
pub source: CacheSource,
}
#[derive(Debug, Clone)]
pub struct MergedModel {
pub catalog_id: Option<String>,
pub model_id: String,
pub display_name: String,
pub size_display: String,
pub downloaded: bool,
pub source: Option<CacheSource>,
pub catalog_model: Option<OndeModel>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum CacheSource {
AppGroup,
HfCache,
}
impl CacheSource {
pub fn label(&self) -> &'static str {
match self {
CacheSource::AppGroup => "Onde",
CacheSource::HfCache => "HF Cache",
}
}
}
#[cfg(target_os = "macos")]
fn app_group_hub() -> Option<PathBuf> {
let p = dirs::home_dir()?
.join("Library")
.join("Group Containers")
.join("group.com.ondeinference.apps")
.join("models")
.join("hub");
p.is_dir().then_some(p)
}
#[cfg(not(target_os = "macos"))]
fn app_group_hub() -> Option<PathBuf> {
None
}
fn hf_home_hub() -> Option<PathBuf> {
for var in ["HF_HUB_CACHE", "HUGGINGFACE_HUB_CACHE"] {
if let Ok(val) = std::env::var(var) {
let p = PathBuf::from(val);
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");
if p.is_dir() {
return Some(p);
}
}
dirs::home_dir()
.map(|h| h.join(".cache").join("huggingface").join("hub"))
.filter(|p| p.is_dir())
}
fn dir_size(path: &PathBuf) -> u64 {
let mut total = 0u64;
if let Ok(entries) = std::fs::read_dir(path) {
for entry in entries.flatten() {
let p = entry.path();
if p.is_file() {
total += p.metadata().map(|m| m.len()).unwrap_or(0);
} else if p.is_dir() {
total += dir_size(&p);
}
}
}
total
}
fn format_size(bytes: u64) -> String {
const KB: u64 = 1_024;
const MB: u64 = 1_024 * KB;
const GB: u64 = 1_024 * MB;
if bytes >= GB {
format!("{:.1} GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.0} 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 scan_dir(dir: &PathBuf, source: CacheSource) -> Vec<LocalModel> {
let entries = match std::fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return Vec::new(),
};
let mut models = Vec::new();
for entry in entries.flatten() {
let path = entry.path();
if !path.is_dir() {
continue;
}
let dir_name = match entry.file_name().into_string() {
Ok(n) => n,
Err(_) => continue,
};
if !dir_name.starts_with("models--") {
continue;
}
let remainder = &dir_name["models--".len()..];
let model_id = remainder.replace("--", "/");
let display_name = model_id
.split('/')
.next_back()
.unwrap_or(&model_id)
.to_string();
let size_bytes = dir_size(&path);
models.push(LocalModel {
model_id,
display_name,
size_display: format_size(size_bytes),
source: source.clone(),
});
}
models
}
pub fn merge_models(catalog: &[OndeModel], local: Vec<LocalModel>) -> Vec<MergedModel> {
let local_by_id: HashMap<&str, &LocalModel> =
local.iter().map(|m| (m.model_id.as_str(), m)).collect();
let mut merged = Vec::new();
let mut seen: HashSet<String> = HashSet::new();
for cm in catalog {
let hf_id = cm.hf_repo_id.as_deref().unwrap_or(cm.id.as_str());
seen.insert(hf_id.to_string());
let local_match = local_by_id.get(hf_id);
let downloaded = local_match.is_some();
let source = local_match.map(|lm| lm.source.clone());
let size_display = if let Some(lm) = local_match {
lm.size_display.clone()
} else if let Some(bytes) = cm.approx_size_bytes {
format_size(bytes as u64)
} else {
"–".to_string()
};
let display_name = cm
.name
.as_deref()
.unwrap_or_else(|| hf_id.split('/').next_back().unwrap_or(hf_id))
.to_string();
merged.push(MergedModel {
catalog_id: Some(cm.id.clone()),
model_id: hf_id.to_string(),
display_name,
size_display,
downloaded,
source,
catalog_model: Some(cm.clone()),
});
}
for lm in &local {
if !seen.contains(lm.model_id.as_str()) {
merged.push(MergedModel {
catalog_id: None,
model_id: lm.model_id.clone(),
display_name: lm.display_name.clone(),
size_display: lm.size_display.clone(),
downloaded: true,
source: Some(lm.source.clone()),
catalog_model: None,
});
}
}
merged.sort_by(|a, b| {
b.downloaded.cmp(&a.downloaded).then(
a.display_name
.to_lowercase()
.cmp(&b.display_name.to_lowercase()),
)
});
merged
}
pub fn list_local_models() -> Vec<LocalModel> {
let mut models = Vec::new();
let mut seen: HashSet<String> = HashSet::new();
if let Some(path) = app_group_hub() {
for m in scan_dir(&path, CacheSource::AppGroup) {
seen.insert(m.model_id.clone());
models.push(m);
}
}
if let Some(path) = hf_home_hub() {
for m in scan_dir(&path, CacheSource::HfCache) {
if !seen.contains(&m.model_id) {
models.push(m);
}
}
}
models.sort_by_key(|m| m.model_id.to_lowercase());
models
}
pub fn preferred_download_hub() -> std::path::PathBuf {
#[cfg(target_os = "macos")]
if let Some(p) = app_group_hub() {
return p;
}
if let Some(p) = hf_home_hub() {
return p;
}
dirs::home_dir()
.map(|h| h.join(".cache").join("huggingface").join("hub"))
.unwrap_or_else(|| std::path::PathBuf::from(".cache/huggingface/hub"))
}