Skip to main content

spn_native/
error.rs

1//! Error types for spn-native.
2
3use spn_core::BackendError;
4use std::path::PathBuf;
5use thiserror::Error;
6
7/// Result type alias for spn-native operations.
8pub type Result<T> = std::result::Result<T, NativeError>;
9
10/// Errors that can occur in spn-native operations.
11#[derive(Error, Debug)]
12pub enum NativeError {
13    /// HTTP request failed.
14    #[error("HTTP request failed: {0}")]
15    Http(#[from] reqwest::Error),
16
17    /// I/O error.
18    #[error("I/O error: {0}")]
19    Io(#[from] std::io::Error),
20
21    /// Model not found on HuggingFace.
22    #[error("Model not found: {repo}/{filename}")]
23    ModelNotFound {
24        /// HuggingFace repository.
25        repo: String,
26        /// Requested filename.
27        filename: String,
28    },
29
30    /// Checksum verification failed.
31    #[error("Checksum mismatch for {path}: expected {expected}, got {actual}")]
32    ChecksumMismatch {
33        /// File path.
34        path: PathBuf,
35        /// Expected SHA256.
36        expected: String,
37        /// Actual SHA256.
38        actual: String,
39    },
40
41    /// Invalid model configuration.
42    #[error("Invalid model configuration: {0}")]
43    InvalidConfig(String),
44
45    /// Download was interrupted.
46    #[error("Download interrupted: {0}")]
47    Interrupted(String),
48
49    /// Storage directory error.
50    #[error("Storage directory error: {0}")]
51    StorageDir(String),
52
53    /// JSON parsing error.
54    #[error("JSON parse error: {0}")]
55    Json(#[from] serde_json::Error),
56
57    // ========================================================================
58    // Inference errors (feature = "inference")
59    // These variants are reserved for future inference features like:
60    // - Detailed error recovery (InferenceFailed)
61    // - Architecture validation (UnsupportedArchitecture)
62    // - GPU/CPU device selection (DeviceError)
63    // - Custom tokenizer support (TokenizerError)
64    // ========================================================================
65    /// Model not loaded.
66    #[error("No model loaded")]
67    ModelNotLoaded,
68
69    /// Inference failed.
70    ///
71    /// Reserved for detailed inference error reporting (e.g., OOM, timeout).
72    #[allow(dead_code)] // Reserved for future inference error handling
73    #[error("Inference failed: {0}")]
74    InferenceFailed(String),
75
76    /// Unsupported model architecture.
77    ///
78    /// Reserved for architecture validation when loading models.
79    #[allow(dead_code)] // Reserved for architecture validation
80    #[error("Unsupported architecture: {0}")]
81    UnsupportedArchitecture(String),
82
83    /// Device error (GPU/CPU).
84    ///
85    /// Reserved for GPU/CPU device selection and error handling.
86    #[allow(dead_code)] // Reserved for device management
87    #[error("Device error: {0}")]
88    DeviceError(String),
89
90    /// Tokenizer error.
91    ///
92    /// Reserved for custom tokenizer support.
93    #[allow(dead_code)] // Reserved for custom tokenizer support
94    #[error("Tokenizer error: {0}")]
95    TokenizerError(String),
96}
97
98impl From<NativeError> for BackendError {
99    fn from(err: NativeError) -> Self {
100        match err {
101            NativeError::Http(e) => BackendError::NetworkError(e.to_string()),
102            NativeError::Io(e) => BackendError::StorageError(e.to_string()),
103            NativeError::ModelNotFound { repo, filename } => {
104                BackendError::ModelNotFound(format!("{repo}/{filename}"))
105            }
106            NativeError::ChecksumMismatch {
107                expected, actual, ..
108            } => BackendError::ChecksumError { expected, actual },
109            NativeError::InvalidConfig(msg) => BackendError::InvalidConfig(msg),
110            NativeError::Interrupted(msg) => BackendError::DownloadError(msg),
111            NativeError::StorageDir(msg) => BackendError::StorageError(msg),
112            NativeError::Json(e) => BackendError::ParseError(e.to_string()),
113            // Inference errors
114            NativeError::ModelNotLoaded => {
115                BackendError::BackendSpecific("No model loaded".to_string())
116            }
117            NativeError::InferenceFailed(msg) => BackendError::BackendSpecific(msg),
118            NativeError::UnsupportedArchitecture(arch) => {
119                BackendError::InvalidConfig(format!("Unsupported architecture: {arch}"))
120            }
121            NativeError::DeviceError(msg) => BackendError::BackendSpecific(msg),
122            NativeError::TokenizerError(msg) => BackendError::BackendSpecific(msg),
123        }
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn test_error_display() {
133        let err = NativeError::ModelNotFound {
134            repo: "test/repo".to_string(),
135            filename: "model.gguf".to_string(),
136        };
137        assert!(err.to_string().contains("test/repo"));
138        assert!(err.to_string().contains("model.gguf"));
139    }
140
141    #[test]
142    fn test_checksum_error() {
143        let err = NativeError::ChecksumMismatch {
144            path: PathBuf::from("/tmp/model.gguf"),
145            expected: "abc123".to_string(),
146            actual: "def456".to_string(),
147        };
148        assert!(err.to_string().contains("abc123"));
149        assert!(err.to_string().contains("def456"));
150    }
151
152    #[test]
153    fn test_into_backend_error() {
154        let err = NativeError::InvalidConfig("bad config".to_string());
155        let backend_err: BackendError = err.into();
156        assert!(matches!(backend_err, BackendError::InvalidConfig(_)));
157    }
158}