pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Network error: {0}")]
Network(String),
#[error("HTTP error {status}: {message}")]
Http { status: u16, message: String },
#[error("Authentication failed: Invalid or missing API key")]
Authentication,
#[error("Rate limit exceeded. Please try again later")]
RateLimit,
#[error("Model not found: {0}")]
ModelNotFound(String),
#[error("Invalid request: {0}")]
InvalidRequest(String),
#[error("Server error: {0}")]
ServerError(String),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("Request timeout after {0} seconds")]
Timeout(u64),
#[error("Connection dropped - possible network instability")]
ConnectionDropped,
#[error("Maximum retries exceeded ({0} attempts)")]
MaxRetriesExceeded(u32),
#[error("API key cannot be empty")]
EmptyApiKey,
#[error("Invalid configuration: {0}")]
InvalidConfig(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("{0}")]
Other(String),
}
impl Error {
pub fn is_retryable(&self) -> bool {
matches!(
self,
Error::Network(_)
| Error::Timeout(_)
| Error::ConnectionDropped
| Error::Http {
status: 502 | 503 | 504 | 520 | 521 | 522 | 523 | 524,
..
}
)
}
pub fn is_rate_limit(&self) -> bool {
matches!(self, Error::RateLimit)
}
pub fn is_auth_error(&self) -> bool {
matches!(self, Error::Authentication)
}
pub fn is_starlink_drop(&self) -> bool {
match self {
Error::ConnectionDropped => true,
Error::Network(msg) => {
let msg_lower = msg.to_lowercase();
msg_lower.contains("connection reset")
|| msg_lower.contains("broken pipe")
|| msg_lower.contains("network unreachable")
}
_ => false,
}
}
pub fn from_reqwest(err: reqwest::Error) -> Self {
let error_string = err.to_string();
let error_lower = error_string.to_lowercase();
if err.is_timeout() || error_lower.contains("timeout") {
return Error::Timeout(30); }
if err.is_connect() || error_lower.contains("connection") {
return Error::Network(error_string);
}
if let Some(status) = err.status() {
let code = status.as_u16();
return match code {
401 => Error::Authentication,
429 => Error::RateLimit,
404 => Error::InvalidRequest("Endpoint not found".to_string()),
400 => Error::InvalidRequest(error_string),
500..=599 => Error::ServerError(error_string),
_ => Error::Http {
status: code,
message: error_string,
},
};
}
Error::Network(error_string)
}
}
impl From<reqwest::Error> for Error {
fn from(err: reqwest::Error) -> Self {
Error::from_reqwest(err)
}
}
impl From<anyhow::Error> for Error {
fn from(err: anyhow::Error) -> Self {
Error::Other(err.to_string())
}
}
impl From<String> for Error {
fn from(s: String) -> Self {
Error::Other(s)
}
}
impl From<&str> for Error {
fn from(s: &str) -> Self {
Error::Other(s.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_is_retryable() {
assert!(Error::Network("connection failed".to_string()).is_retryable());
assert!(Error::Timeout(30).is_retryable());
assert!(Error::ConnectionDropped.is_retryable());
assert!(!Error::Authentication.is_retryable());
assert!(!Error::RateLimit.is_retryable());
}
#[test]
fn test_error_is_starlink_drop() {
assert!(Error::ConnectionDropped.is_starlink_drop());
assert!(Error::Network("connection reset by peer".to_string()).is_starlink_drop());
assert!(Error::Network("broken pipe".to_string()).is_starlink_drop());
assert!(!Error::Authentication.is_starlink_drop());
}
#[test]
fn test_error_display() {
let err = Error::Authentication;
assert_eq!(
err.to_string(),
"Authentication failed: Invalid or missing API key"
);
let err = Error::ModelNotFound("grok-99".to_string());
assert_eq!(err.to_string(), "Model not found: grok-99");
}
}