Skip to main content

oxirs_graphrag/model_loader/
registry.rs

1//! Thread-safe model registry for GGUF model metadata.
2//!
3//! The registry stores [`ModelInfo`] records (path + parsed metadata) keyed by
4//! name-based [`ModelHandle`]s.  No tensor weights are held in memory.
5
6use std::collections::HashMap;
7use std::path::PathBuf;
8use std::sync::RwLock;
9
10use super::gguf_parser::{GgufMetadata, GgufParseError, GgufParser};
11
12// ─── ModelHandle ─────────────────────────────────────────────────────────────
13
14/// An opaque handle to a registered model, identified by name.
15#[derive(Debug, Clone, PartialEq, Eq, Hash)]
16pub struct ModelHandle(String);
17
18impl ModelHandle {
19    /// Return the model name.
20    pub fn name(&self) -> &str {
21        &self.0
22    }
23}
24
25// ─── ModelInfo ───────────────────────────────────────────────────────────────
26
27/// Lightweight model record held by the registry.
28///
29/// Contains only the file path and the parsed GGUF metadata header; tensor
30/// weight data is never loaded.
31#[derive(Debug, Clone)]
32pub struct ModelInfo {
33    /// Opaque handle for this model.
34    pub handle: ModelHandle,
35    /// Absolute path to the `.gguf` file.
36    pub path: PathBuf,
37    /// Parsed GGUF header metadata.
38    pub metadata: GgufMetadata,
39}
40
41impl ModelInfo {
42    /// Return the model's hidden / embedding dimension, if declared.
43    pub fn embedding_dim(&self) -> Option<usize> {
44        self.metadata.arch.embedding_length.map(|v| v as usize)
45    }
46
47    /// Return the vocabulary size, if declared.
48    pub fn vocab_size(&self) -> Option<usize> {
49        self.metadata.arch.vocab_size.map(|v| v as usize)
50    }
51}
52
53// ─── RegistryError ───────────────────────────────────────────────────────────
54
55/// Errors produced by the model registry.
56#[derive(Debug, thiserror::Error)]
57pub enum RegistryError {
58    /// A model with this name is already registered.
59    #[error("model '{0}' already registered")]
60    AlreadyRegistered(String),
61    /// No model with this name was found.
62    #[error("model '{0}' not found")]
63    NotFound(String),
64    /// Failed to parse the GGUF file.
65    #[error("GGUF parse error: {0}")]
66    ParseError(#[from] GgufParseError),
67}
68
69// ─── ModelRegistry ───────────────────────────────────────────────────────────
70
71/// Thread-safe registry of loaded model metadata.
72///
73/// Models are keyed by name; each registration parses (or accepts) GGUF
74/// metadata without loading tensor weights.
75///
76/// # Example
77///
78/// ```rust
79/// # #[cfg(feature = "gguf-loader")]
80/// # {
81/// use oxirs_graphrag::model_loader::{ModelRegistry, GgufMetadata, GgufParser};
82/// use std::path::PathBuf;
83///
84/// let registry = ModelRegistry::new();
85/// // In real usage you would supply a path to a .gguf file:
86/// // let handle = registry.register("llama-3-8b", PathBuf::from("model.gguf")).unwrap();
87/// assert!(registry.is_empty());
88/// # }
89/// ```
90#[derive(Default)]
91pub struct ModelRegistry {
92    models: RwLock<HashMap<String, ModelInfo>>,
93}
94
95impl ModelRegistry {
96    /// Create an empty registry.
97    pub fn new() -> Self {
98        Self::default()
99    }
100
101    /// Register a model by parsing its GGUF file.
102    ///
103    /// Returns an error if `name` is already registered or the file cannot be
104    /// parsed.
105    pub fn register(&self, name: &str, path: PathBuf) -> Result<ModelHandle, RegistryError> {
106        let metadata = GgufParser::parse_file(&path)?;
107        self.register_with_metadata(name, path, metadata)
108    }
109
110    /// Register a model with pre-parsed metadata.
111    ///
112    /// Useful for tests that construct synthetic metadata without a real file.
113    pub fn register_with_metadata(
114        &self,
115        name: &str,
116        path: PathBuf,
117        metadata: GgufMetadata,
118    ) -> Result<ModelHandle, RegistryError> {
119        let mut map = self.models.write().expect("registry lock poisoned");
120        if map.contains_key(name) {
121            return Err(RegistryError::AlreadyRegistered(name.to_owned()));
122        }
123        let handle = ModelHandle(name.to_owned());
124        let info = ModelInfo {
125            handle: handle.clone(),
126            path,
127            metadata,
128        };
129        map.insert(name.to_owned(), info);
130        Ok(handle)
131    }
132
133    /// Look up a model by its handle.
134    pub fn get(&self, handle: &ModelHandle) -> Option<ModelInfo> {
135        let map = self.models.read().expect("registry lock poisoned");
136        map.get(handle.name()).cloned()
137    }
138
139    /// Look up a model by name.
140    pub fn get_by_name(&self, name: &str) -> Option<ModelInfo> {
141        let map = self.models.read().expect("registry lock poisoned");
142        map.get(name).cloned()
143    }
144
145    /// List all registered model handles.
146    pub fn list(&self) -> Vec<ModelHandle> {
147        let map = self.models.read().expect("registry lock poisoned");
148        map.values().map(|info| info.handle.clone()).collect()
149    }
150
151    /// Remove a model from the registry.
152    ///
153    /// Returns `true` if the model existed and was removed.
154    pub fn remove(&self, handle: &ModelHandle) -> bool {
155        let mut map = self.models.write().expect("registry lock poisoned");
156        map.remove(handle.name()).is_some()
157    }
158
159    /// Return the number of registered models.
160    pub fn len(&self) -> usize {
161        let map = self.models.read().expect("registry lock poisoned");
162        map.len()
163    }
164
165    /// Return `true` if no models are registered.
166    pub fn is_empty(&self) -> bool {
167        self.len() == 0
168    }
169}