use thiserror::Error;
#[derive(Error, Debug)]
pub enum ProviderError {
#[error("request failed: {0}")]
RequestFailed(String),
#[error("authentication failed: {0}")]
AuthFailed(String),
#[error("rate limited: retry after {retry_after_ms}ms")]
RateLimited {
retry_after_ms: u64,
},
#[error("model not found: {0}")]
ModelNotFound(String),
#[error("connection failed: {0}")]
Connect(String),
#[error("provider not configured: {0}")]
NotConfigured(String),
#[error("invalid response: {0}")]
InvalidResponse(String),
#[error("timeout")]
Timeout,
#[error("http error: {0}")]
Http(#[from] reqwest::Error),
#[error("json error: {0}")]
Json(#[from] serde_json::Error),
#[error("server error {status}: {body}")]
ServerError {
status: u16,
body: String,
},
#[error("all providers exhausted: {}", attempts.join("; "))]
AllProvidersExhausted {
attempts: Vec<String>,
},
}
pub type Result<T> = std::result::Result<T, ProviderError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn display_request_failed() {
let err = ProviderError::RequestFailed("connection reset".into());
assert_eq!(err.to_string(), "request failed: connection reset");
}
#[test]
fn display_auth_failed() {
let err = ProviderError::AuthFailed("invalid token".into());
assert_eq!(err.to_string(), "authentication failed: invalid token");
}
#[test]
fn display_rate_limited() {
let err = ProviderError::RateLimited {
retry_after_ms: 5000,
};
assert_eq!(err.to_string(), "rate limited: retry after 5000ms");
}
#[test]
fn display_model_not_found() {
let err = ProviderError::ModelNotFound("gpt-5-turbo".into());
assert_eq!(err.to_string(), "model not found: gpt-5-turbo");
}
#[test]
fn display_not_configured() {
let err = ProviderError::NotConfigured("set OPENAI_API_KEY env var".into());
assert_eq!(
err.to_string(),
"provider not configured: set OPENAI_API_KEY env var"
);
}
#[test]
fn display_invalid_response() {
let err = ProviderError::InvalidResponse("missing choices field".into());
assert_eq!(err.to_string(), "invalid response: missing choices field");
}
#[test]
fn display_timeout() {
let err = ProviderError::Timeout;
assert_eq!(err.to_string(), "timeout");
}
#[test]
fn json_error_from_conversion() {
let bad_json = serde_json::from_str::<serde_json::Value>("not json");
let serde_err = bad_json.unwrap_err();
let provider_err: ProviderError = serde_err.into();
assert!(provider_err.to_string().starts_with("json error:"));
}
#[test]
fn result_type_alias_works() {
let ok: Result<i32> = Ok(42);
assert!(ok.is_ok());
let err: Result<i32> = Err(ProviderError::Timeout);
assert!(err.is_err());
}
#[test]
fn display_server_error() {
let err = ProviderError::ServerError {
status: 503,
body: "service unavailable".into(),
};
assert_eq!(err.to_string(), "server error 503: service unavailable");
}
#[test]
fn display_all_providers_exhausted() {
let err = ProviderError::AllProvidersExhausted {
attempts: vec!["openai: rate limited".into(), "anthropic: timeout".into()],
};
let msg = err.to_string();
assert!(msg.contains("all providers exhausted"));
assert!(msg.contains("openai: rate limited"));
assert!(msg.contains("anthropic: timeout"));
}
}