Skip to main content

ferrotensor_hub/
lib.rs

1//! Ferrotensor Hub - HuggingFace Model Hub Integration
2//!
3//! Download and load pre-trained models from HuggingFace with a single line of code.
4//!
5//! # Example
6//! ```no_run
7//! use ferrotensor_hub::{FerrotensorHub, ModelConfig};
8//!
9//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
10//! // Resolve a model already downloaded to the Hugging Face cache
11//! let hub = FerrotensorHub::new();
12//! let model_path = hub.get_model_path("meta-llama/Llama-3.2-1B");
13//! let config = ModelConfig::from_model_dir(&model_path)?;
14//! println!("{} layers", config.num_hidden_layers);
15//! # Ok(())
16//! # }
17//! ```
18
19#![warn(missing_docs)]
20
21use std::fs;
22use std::path::PathBuf;
23
24pub mod config;
25pub mod models;
26
27pub use config::ModelConfig;
28pub use models::ModelType;
29
30/// Errors that can occur when using FerrotensorHub
31#[derive(Debug)]
32pub enum HubError {
33    /// Underlying I/O failure.
34    Io(std::io::Error),
35    /// Failure parsing a JSON file.
36    Json(serde_json::Error),
37    /// The model directory or file was not found.
38    ModelNotFound(String),
39    /// The model type is not supported.
40    UnsupportedModel(String),
41}
42
43impl std::fmt::Display for HubError {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        match self {
46            Self::Io(e) => write!(f, "IO error: {}", e),
47            Self::Json(e) => write!(f, "JSON error: {}", e),
48            Self::ModelNotFound(m) => write!(f, "Model not found: {}", m),
49            Self::UnsupportedModel(m) => write!(f, "Unsupported model: {}", m),
50        }
51    }
52}
53
54impl std::error::Error for HubError {}
55
56impl From<std::io::Error> for HubError {
57    fn from(e: std::io::Error) -> Self {
58        Self::Io(e)
59    }
60}
61
62impl From<serde_json::Error> for HubError {
63    fn from(e: serde_json::Error) -> Self {
64        Self::Json(e)
65    }
66}
67
68/// Result type for hub operations.
69pub type HubResult<T> = Result<T, HubError>;
70
71/// Main entry point for loading models from HuggingFace Hub.
72///
73/// Currently supports loading from local directories.
74/// Download models using `huggingface-cli download` or the Python `huggingface_hub` library.
75///
76/// # Example
77/// ```no_run
78/// # use ferrotensor_hub::FerrotensorHub;
79/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
80/// let hub = FerrotensorHub::new();
81/// let config = hub.load_config("meta-llama/Llama-3.2-1B")?;
82/// println!("Model has {} layers", config.num_hidden_layers);
83/// # Ok(())
84/// # }
85/// ```
86pub struct FerrotensorHub {
87    cache_dir: PathBuf,
88}
89
90impl FerrotensorHub {
91    /// Create a new FerrotensorHub instance with default cache directory.
92    ///
93    /// Default cache: `~/.cache/huggingface/hub/models--{org}--{model}`
94    pub fn new() -> Self {
95        let cache_dir = dirs_cache_dir().join("huggingface").join("hub");
96
97        Self { cache_dir }
98    }
99
100    /// Create a FerrotensorHub with a custom cache directory.
101    pub fn with_cache_dir(cache_dir: PathBuf) -> Self {
102        Self { cache_dir }
103    }
104
105    /// Get the path to a cached model.
106    ///
107    /// Compatible with HuggingFace cache format.
108    pub fn get_model_path(&self, model_id: &str) -> PathBuf {
109        // HuggingFace cache format: models--{org}--{model}
110        let cache_name = format!("models--{}", model_id.replace("/", "--"));
111        self.cache_dir.join(cache_name).join("snapshots")
112    }
113
114    /// Find the latest snapshot of a model.
115    pub fn find_latest_snapshot(&self, model_id: &str) -> HubResult<PathBuf> {
116        let snapshots_dir = self.get_model_path(model_id);
117
118        if !snapshots_dir.exists() {
119            return Err(HubError::ModelNotFound(format!(
120                "Model {} not found. Download with: huggingface-cli download {}",
121                model_id, model_id
122            )));
123        }
124
125        // Find first snapshot directory
126        let entries = fs::read_dir(&snapshots_dir)?;
127        for entry in entries {
128            let entry = entry?;
129            if entry.file_type()?.is_dir() {
130                return Ok(entry.path());
131            }
132        }
133
134        Err(HubError::ModelNotFound(format!(
135            "No snapshots found for {}",
136            model_id
137        )))
138    }
139
140    /// Load model configuration.
141    pub fn load_config(&self, model_id: &str) -> HubResult<ModelConfig> {
142        let snapshot = self.find_latest_snapshot(model_id)?;
143        ModelConfig::from_file(&snapshot.join("config.json"))
144    }
145
146    /// Check if a model is cached locally.
147    pub fn is_cached(&self, model_id: &str) -> bool {
148        self.get_model_path(model_id).exists()
149    }
150
151    /// List all cached models.
152    pub fn list_cached(&self) -> Vec<String> {
153        if !self.cache_dir.exists() {
154            return vec![];
155        }
156
157        fs::read_dir(&self.cache_dir)
158            .map(|entries| {
159                entries
160                    .filter_map(|e| e.ok())
161                    .filter_map(|e| e.file_name().into_string().ok())
162                    .filter(|name| name.starts_with("models--"))
163                    .map(|name| name.trim_start_matches("models--").replace("--", "/"))
164                    .collect()
165            })
166            .unwrap_or_default()
167    }
168
169    /// Get safetensors file paths for a model.
170    pub fn get_weight_files(&self, model_id: &str) -> HubResult<Vec<PathBuf>> {
171        let snapshot = self.find_latest_snapshot(model_id)?;
172
173        let mut files = Vec::new();
174        for entry in fs::read_dir(&snapshot)? {
175            let entry = entry?;
176            let path = entry.path();
177            if path.extension().is_some_and(|e| e == "safetensors") {
178                files.push(path);
179            }
180        }
181
182        // Sort to ensure consistent order
183        files.sort();
184        Ok(files)
185    }
186}
187
188impl Default for FerrotensorHub {
189    fn default() -> Self {
190        Self::new()
191    }
192}
193
194/// Get cache directory (fallback if dirs crate not available)
195fn dirs_cache_dir() -> PathBuf {
196    std::env::var("HOME")
197        .map(|h| PathBuf::from(h).join(".cache"))
198        .unwrap_or_else(|_| PathBuf::from(".cache"))
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    #[test]
206    fn test_hub_creation() {
207        let hub = FerrotensorHub::new();
208        assert!(hub.cache_dir.to_string_lossy().contains("huggingface"));
209    }
210
211    #[test]
212    fn test_model_path() {
213        let hub = FerrotensorHub::new();
214        let path = hub.get_model_path("meta-llama/Llama-3.2-1B");
215        assert!(path
216            .to_string_lossy()
217            .contains("models--meta-llama--Llama-3.2-1B"));
218    }
219}