1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
//! Unified Model Loader
//!
//! Per spec §3.2 and §5: Unified loading for APR, GGUF, and SafeTensors formats.
//!
//! ## Jidoka (Built-in Quality)
//!
//! - Format auto-detection from magic bytes
//! - CRC32 verification for APR format
//! - Header validation for SafeTensors
//! - Graceful error handling with detailed messages
//!
//! ## APR Format Support (First-class)
//!
//! Per spec §3.1: APR is the primary format for classical ML models from aprender.
//! Supports all 18 model types:
//!
//! | Type | Description |
//! |------|-------------|
//! | `LinearRegression` | OLS/Ridge/Lasso |
//! | `LogisticRegression` | Binary/Multinomial classification |
//! | `DecisionTree` | CART/ID3 |
//! | `RandomForest` | Bagging ensemble |
//! | `GradientBoosting` | Boosting ensemble |
//! | `KMeans` | Lloyd's clustering |
//! | `PCA` | Dimensionality reduction |
//! | `NaiveBayes` | Gaussian NB |
//! | `KNN` | k-Nearest Neighbors |
//! | `SVM` | Linear SVM |
//! | `NgramLM` | N-gram language model |
//! | `TFIDF` | TF-IDF vectorizer |
//! | `CountVectorizer` | Count vectorizer |
//! | `NeuralSequential` | Feed-forward NN |
//! | `NeuralCustom` | Custom architecture |
//! | `ContentRecommender` | Content-based rec |
//! | `MixtureOfExperts` | Sparse/dense MoE |
//! | `Custom` | User-defined |
//!
//! ## GGUF Support (Backwards Compatible)
//!
//! Per spec §3.3: GGUF for LLM inference with llama.cpp compatibility.
//!
//! ## SafeTensors Support (Backwards Compatible)
//!
//! Per spec §3.4: SafeTensors for HuggingFace model weights.
use std::path::Path;
use crate::format::{detect_and_verify_format, detect_format, FormatError, ModelFormat};
/// Model loading errors
#[derive(Debug, Clone)]
pub enum LoadError {
/// Format detection failed
FormatError(FormatError),
/// File I/O error
IoError(String),
/// Model parsing error
ParseError(String),
/// Unsupported model type for serving
UnsupportedType(String),
/// CRC32 checksum mismatch (APR)
IntegrityError(String),
/// Model type mismatch (requested vs detected)
TypeMismatch {
/// Expected model type
expected: String,
/// Actual model type in file
actual: String,
},
}
impl std::fmt::Display for LoadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::FormatError(e) => write!(f, "Format detection error: {e}"),
Self::IoError(msg) => write!(f, "I/O error: {msg}"),
Self::ParseError(msg) => write!(f, "Parse error: {msg}"),
Self::UnsupportedType(t) => write!(f, "Unsupported model type: {t}"),
Self::IntegrityError(msg) => write!(f, "Integrity check failed: {msg}"),
Self::TypeMismatch { expected, actual } => {
write!(f, "Model type mismatch: expected {expected}, got {actual}")
},
}
}
}
impl std::error::Error for LoadError {}
impl From<FormatError> for LoadError {
fn from(e: FormatError) -> Self {
Self::FormatError(e)
}
}
impl From<std::io::Error> for LoadError {
fn from(e: std::io::Error) -> Self {
Self::IoError(e.to_string())
}
}
/// Model metadata extracted during loading
#[derive(Debug, Clone)]
pub struct ModelMetadata {
/// Detected format
pub format: ModelFormat,
/// Model type (if detected)
pub model_type: Option<String>,
/// Model version
pub version: Option<String>,
/// Input dimensions (for validation)
pub input_dim: Option<usize>,
/// Output dimensions
pub output_dim: Option<usize>,
/// File size in bytes
pub file_size: u64,
}
impl ModelMetadata {
/// Create new metadata with format only
#[must_use]
pub fn new(format: ModelFormat) -> Self {
Self {
format,
model_type: None,
version: None,
input_dim: None,
output_dim: None,
file_size: 0,
}
}
/// Set model type
#[must_use]
pub fn with_model_type(mut self, model_type: impl Into<String>) -> Self {
self.model_type = Some(model_type.into());
self
}
/// Set version
#[must_use]
pub fn with_version(mut self, version: impl Into<String>) -> Self {
self.version = Some(version.into());
self
}
/// Set input dimensions
#[must_use]
pub fn with_input_dim(mut self, dim: usize) -> Self {
self.input_dim = Some(dim);
self
}
/// Set output dimensions
#[must_use]
pub fn with_output_dim(mut self, dim: usize) -> Self {
self.output_dim = Some(dim);
self
}
/// Set file size
#[must_use]
pub fn with_file_size(mut self, size: u64) -> Self {
self.file_size = size;
self
}
}
/// Detect model format from file path and contents
///
/// Per spec §3.2: Jidoka - verify both path and magic bytes match.
///
/// # Arguments
///
/// * `path` - Path to model file
///
/// # Returns
///
/// Model metadata with detected format
///
/// # Errors
///
/// Returns error if:
/// - File cannot be read
/// - Format cannot be detected
/// - Extension doesn't match magic bytes
///
/// # Example
///
/// ```rust,ignore
/// use realizar::model_loader::detect_model;
/// use std::path::Path;
///
/// let metadata = detect_model(Path::new("model.apr"))?;
/// assert_eq!(metadata.format, ModelFormat::Apr);
/// ```
pub fn detect_model(path: &Path) -> Result<ModelMetadata, LoadError> {
// ALB-099: Read only 8 bytes for magic detection (was reading entire file)
use std::io::Read;
let mut file = std::fs::File::open(path)?;
let file_size = file.metadata().map(|m| m.len()).unwrap_or(0);
let mut magic = [0u8; 8];
let bytes_read = file.read(&mut magic)?;
if bytes_read < 8 {
return Err(LoadError::ParseError(format!(
"File too small: {} bytes",
bytes_read
)));
}
let format = detect_and_verify_format(path, &magic)?;
Ok(ModelMetadata::new(format).with_file_size(file_size))
}
/// Detect model format from bytes only (no path verification)
///
/// Useful for embedded models via `include_bytes!()`.
///
/// # Arguments
///
/// * `data` - Model file bytes
///
/// # Returns
///
/// Model metadata with detected format
///
/// # Errors
///
/// Returns error if:
/// - Data is too small for format detection (<8 bytes)
/// - Format cannot be detected from magic bytes
///
/// # Example
///
/// ```rust,ignore
/// use realizar::model_loader::detect_model_from_bytes;
///
/// const MODEL: &[u8] = include_bytes!("../models/model.apr");
/// let metadata = detect_model_from_bytes(MODEL)?;
/// ```
pub fn detect_model_from_bytes(data: &[u8]) -> Result<ModelMetadata, LoadError> {
if data.len() < 8 {
return Err(LoadError::ParseError(format!(
"Data too small: {} bytes",
data.len()
)));
}
let format = detect_format(data.get(..8).expect("len >= 8 checked above"))?;
Ok(ModelMetadata::new(format).with_file_size(data.len() as u64))
}
/// Extract model type from APR v2 JSON metadata
///
/// Reads the metadata offset/size from the header, parses JSON, and
/// returns model_type or model.architecture field.
fn read_apr_v2_model_type(data: &[u8]) -> Option<String> {
if data.len() < 64 {
return Some("Transformer".to_string()); // Default for incomplete header
}
let metadata_offset = u64::from_le_bytes([
data[12], data[13], data[14], data[15], data[16], data[17], data[18], data[19],
]) as usize;
let metadata_size = u32::from_le_bytes([data[20], data[21], data[22], data[23]]) as usize;
if metadata_offset + metadata_size > data.len() || metadata_size == 0 {
return Some("Transformer".to_string());
}
let metadata_bytes = &data[metadata_offset..metadata_offset + metadata_size];
let metadata_str = std::str::from_utf8(metadata_bytes).ok()?;
let json: serde_json::Value = serde_json::from_str(metadata_str).ok()?;
// Check for model_type field in JSON metadata
if let Some(model_type) = json.get("model_type").and_then(|v| v.as_str()) {
if !model_type.is_empty() {
return Some(model_type.to_string());
}
}
// Check for model.architecture field (from GGUF import)
if let Some(arch) = json.get("model.architecture").and_then(|v| v.as_str()) {
return Some(format!("Transformer({})", arch));
}
Some("Transformer".to_string())
}
/// Map APR v1 type ID to model type name
fn read_apr_v1_model_type(type_id: u16) -> Option<&'static str> {
match type_id {
0x0001 => Some("LinearRegression"),
0x0002 => Some("LogisticRegression"),
0x0003 => Some("DecisionTree"),
0x0004 => Some("RandomForest"),
0x0005 => Some("GradientBoosting"),
0x0006 => Some("KMeans"),
0x0007 => Some("PCA"),
0x0008 => Some("NaiveBayes"),
0x0009 => Some("KNN"),
0x000A => Some("SVM"),
0x0010 => Some("NgramLM"),
0x0011 => Some("TFIDF"),
0x0012 => Some("CountVectorizer"),
0x0020 => Some("NeuralSequential"),
0x0021 => Some("NeuralCustom"),
0x0030 => Some("ContentRecommender"),
0x0040 => Some("MixtureOfExperts"),
0x00FF => Some("Custom"),
_ => None,
}
}
/// Load APR model type from metadata bytes
///
/// Supports both APR v1 (type in header) and APR v2 (type in JSON metadata).
///
/// # Arguments
///
/// * `data` - APR file bytes (at least 8 bytes)
///
/// # Returns
///
/// APR model type string (e.g., "LogisticRegression", "Transformer")
pub fn read_apr_model_type(data: &[u8]) -> Option<String> {
if data.len() < 8 {
return None;
}
// APR v2 magic: "APR\0" (0x41, 0x50, 0x52, 0x00)
if data[0..4] == [0x41, 0x50, 0x52, 0x00] {
return read_apr_v2_model_type(data);
}
// APR v1 header layout: APRN (4 bytes) + type_id (2 bytes) + version (2 bytes)
let type_id = u16::from_le_bytes([data[4], data[5]]);
read_apr_v1_model_type(type_id).map(String::from)
}
/// Validate that loaded model matches expected type
///
/// Per Jidoka: fail fast if type mismatch.
///
/// # Arguments
///
/// * `expected` - Expected model type
/// * `actual` - Actual model type from file
///
/// # Returns
///
/// Ok if types match, Err otherwise
///
/// # Errors
///
/// Returns `LoadError::TypeMismatch` if expected and actual types differ.
pub fn validate_model_type(expected: &str, actual: &str) -> Result<(), LoadError> {
if expected != actual {
return Err(LoadError::TypeMismatch {
expected: expected.to_string(),
actual: actual.to_string(),
});
}
Ok(())
}
include!("model_loader_load_error.rs");