use thiserror::Error;
#[derive(Debug, Error)]
pub enum BackendError {
#[error("backend unavailable: {0}")]
Unavailable(String),
#[error("rate limited")]
RateLimited,
#[error("auth failed: {0}")]
AuthFailed(String),
#[error("model error: {0}")]
ModelError(String),
#[error("model file {file} has been modified (expected {expected}, got {actual})")]
ModelIntegrityFailure {
file: String,
expected: String,
actual: String,
},
#[error("invalid request: {0}")]
Invalid(String),
}
#[derive(Debug, Error)]
pub enum SummarizerError {
#[error("no such backend: {name}")]
NoSuchBackend { name: String },
#[error("no extractive backend configured for fallback")]
NoExtractiveBackendForFallback,
#[error("backend {name} unavailable: {reason}")]
BackendUnavailable { name: String, reason: String },
#[error("backend {name} rate limited")]
RateLimited { name: String },
#[error("backend {name} auth failed: {reason}")]
AuthFailed { name: String, reason: String },
#[error("backend {name} model error: {reason}")]
ModelError { name: String, reason: String },
#[error("invalid request to backend {name}: {reason}")]
InvalidRequest { name: String, reason: String },
#[error("local-inference backend requires the `local-inference` cargo feature")]
LocalFeatureNotCompiled,
#[error("storage error: {0}")]
Storage(#[from] crate::storage::StorageError),
#[error("token counting error: {0}")]
Tokenizer(#[from] crate::tokenizer::TokenizerError),
}
impl SummarizerError {
pub fn from_backend(name: &str, e: BackendError) -> Self {
match e {
BackendError::Unavailable(r) => SummarizerError::BackendUnavailable {
name: name.to_string(),
reason: r,
},
BackendError::Invalid(r) => SummarizerError::InvalidRequest {
name: name.to_string(),
reason: r,
},
BackendError::RateLimited => SummarizerError::RateLimited {
name: name.to_string(),
},
BackendError::AuthFailed(r) => SummarizerError::AuthFailed {
name: name.to_string(),
reason: r,
},
BackendError::ModelError(r) => SummarizerError::ModelError {
name: name.to_string(),
reason: r,
},
BackendError::ModelIntegrityFailure {
file,
expected,
actual,
} => SummarizerError::ModelError {
name: name.to_string(),
reason: format!(
"model file {file} has been modified (expected {expected}, got {actual})"
),
},
}
}
pub fn fallback_reason(&self) -> &'static str {
match self {
SummarizerError::BackendUnavailable { .. } => "backend_unavailable",
SummarizerError::InvalidRequest { .. } => "invalid_request",
SummarizerError::RateLimited { .. } => "rate_limited",
SummarizerError::AuthFailed { .. } => "auth_failed",
SummarizerError::ModelError { .. } => "model_error",
other => {
debug_assert!(
false,
"fallback_reason called on non-fallback-path variant: {other}",
);
"other"
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_backend_maps_each_variant() {
let cases = [
(
BackendError::Unavailable("net".into()),
"backend_unavailable",
),
(BackendError::RateLimited, "rate_limited"),
(BackendError::AuthFailed("401".into()), "auth_failed"),
(BackendError::ModelError("bad".into()), "model_error"),
(BackendError::Invalid("empty".into()), "invalid_request"),
];
for (be, expected_reason) in cases {
let e = SummarizerError::from_backend("fast", be);
assert_eq!(e.fallback_reason(), expected_reason, "for {e}");
}
}
#[test]
fn storage_error_converts_via_from() {
let storage_err =
crate::storage::StorageError::Backend(tokio_rusqlite::Error::ConnectionClosed);
let e: SummarizerError = storage_err.into();
assert!(matches!(e, SummarizerError::Storage(_)));
}
#[test]
fn tokenizer_error_converts_via_from() {
let tok_err = crate::tokenizer::TokenizerError::UnknownFamily("test".to_string());
let e: SummarizerError = tok_err.into();
assert!(matches!(e, SummarizerError::Tokenizer(_)));
}
}