#![warn(missing_docs)]
use std::fs;
use std::path::PathBuf;
pub mod config;
pub mod models;
pub use config::ModelConfig;
pub use models::ModelType;
#[derive(Debug)]
pub enum HubError {
Io(std::io::Error),
Json(serde_json::Error),
ModelNotFound(String),
UnsupportedModel(String),
}
impl std::fmt::Display for HubError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io(e) => write!(f, "IO error: {}", e),
Self::Json(e) => write!(f, "JSON error: {}", e),
Self::ModelNotFound(m) => write!(f, "Model not found: {}", m),
Self::UnsupportedModel(m) => write!(f, "Unsupported model: {}", m),
}
}
}
impl std::error::Error for HubError {}
impl From<std::io::Error> for HubError {
fn from(e: std::io::Error) -> Self {
Self::Io(e)
}
}
impl From<serde_json::Error> for HubError {
fn from(e: serde_json::Error) -> Self {
Self::Json(e)
}
}
pub type HubResult<T> = Result<T, HubError>;
pub struct FerrotensorHub {
cache_dir: PathBuf,
}
impl FerrotensorHub {
pub fn new() -> Self {
let cache_dir = dirs_cache_dir().join("huggingface").join("hub");
Self { cache_dir }
}
pub fn with_cache_dir(cache_dir: PathBuf) -> Self {
Self { cache_dir }
}
pub fn get_model_path(&self, model_id: &str) -> PathBuf {
let cache_name = format!("models--{}", model_id.replace("/", "--"));
self.cache_dir.join(cache_name).join("snapshots")
}
pub fn find_latest_snapshot(&self, model_id: &str) -> HubResult<PathBuf> {
let snapshots_dir = self.get_model_path(model_id);
if !snapshots_dir.exists() {
return Err(HubError::ModelNotFound(format!(
"Model {} not found. Download with: huggingface-cli download {}",
model_id, model_id
)));
}
let entries = fs::read_dir(&snapshots_dir)?;
for entry in entries {
let entry = entry?;
if entry.file_type()?.is_dir() {
return Ok(entry.path());
}
}
Err(HubError::ModelNotFound(format!(
"No snapshots found for {}",
model_id
)))
}
pub fn load_config(&self, model_id: &str) -> HubResult<ModelConfig> {
let snapshot = self.find_latest_snapshot(model_id)?;
ModelConfig::from_file(&snapshot.join("config.json"))
}
pub fn is_cached(&self, model_id: &str) -> bool {
self.get_model_path(model_id).exists()
}
pub fn list_cached(&self) -> Vec<String> {
if !self.cache_dir.exists() {
return vec![];
}
fs::read_dir(&self.cache_dir)
.map(|entries| {
entries
.filter_map(|e| e.ok())
.filter_map(|e| e.file_name().into_string().ok())
.filter(|name| name.starts_with("models--"))
.map(|name| name.trim_start_matches("models--").replace("--", "/"))
.collect()
})
.unwrap_or_default()
}
pub fn get_weight_files(&self, model_id: &str) -> HubResult<Vec<PathBuf>> {
let snapshot = self.find_latest_snapshot(model_id)?;
let mut files = Vec::new();
for entry in fs::read_dir(&snapshot)? {
let entry = entry?;
let path = entry.path();
if path.extension().is_some_and(|e| e == "safetensors") {
files.push(path);
}
}
files.sort();
Ok(files)
}
}
impl Default for FerrotensorHub {
fn default() -> Self {
Self::new()
}
}
fn dirs_cache_dir() -> PathBuf {
std::env::var("HOME")
.map(|h| PathBuf::from(h).join(".cache"))
.unwrap_or_else(|_| PathBuf::from(".cache"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hub_creation() {
let hub = FerrotensorHub::new();
assert!(hub.cache_dir.to_string_lossy().contains("huggingface"));
}
#[test]
fn test_model_path() {
let hub = FerrotensorHub::new();
let path = hub.get_model_path("meta-llama/Llama-3.2-1B");
assert!(path
.to_string_lossy()
.contains("models--meta-llama--Llama-3.2-1B"));
}
}