ferrous_llm_openai/
error.rs

1//! OpenAI-specific error types.
2
3use ferrous_llm_core::ProviderError;
4use std::time::Duration;
5use thiserror::Error;
6
7/// OpenAI-specific error types.
8#[derive(Debug, Error)]
9pub enum OpenAIError {
10    /// Authentication failed
11    #[error("Authentication failed: {message}")]
12    Authentication { message: String },
13
14    /// Rate limited
15    #[error("Rate limited: retry after {retry_after:?}")]
16    RateLimit { retry_after: Option<Duration> },
17
18    /// Invalid request
19    #[error("Invalid request: {message}")]
20    InvalidRequest { message: String },
21
22    /// Service unavailable
23    #[error("Service unavailable: {message}")]
24    ServiceUnavailable { message: String },
25
26    /// Content filtered
27    #[error("Content filtered: {message}")]
28    ContentFiltered { message: String },
29
30    /// Model not found
31    #[error("Model not found: {model}")]
32    ModelNotFound { model: String },
33
34    /// Insufficient quota
35    #[error("Insufficient quota: {message}")]
36    InsufficientQuota { message: String },
37
38    /// Network error
39    #[error("Network error: {source}")]
40    Network {
41        #[from]
42        source: reqwest::Error,
43    },
44
45    /// JSON parsing error
46    #[error("JSON parsing error: {source}")]
47    Json {
48        #[from]
49        source: serde_json::Error,
50    },
51
52    /// Configuration error
53    #[error("Configuration error: {source}")]
54    Config {
55        #[from]
56        source: ferrous_llm_core::ConfigError,
57    },
58
59    /// Generic error
60    #[error("OpenAI error: {message}")]
61    Other { message: String },
62}
63
64impl ProviderError for OpenAIError {
65    fn error_code(&self) -> Option<&str> {
66        match self {
67            Self::Authentication { .. } => Some("authentication_failed"),
68            Self::RateLimit { .. } => Some("rate_limit_exceeded"),
69            Self::InvalidRequest { .. } => Some("invalid_request"),
70            Self::ServiceUnavailable { .. } => Some("service_unavailable"),
71            Self::ContentFiltered { .. } => Some("content_filtered"),
72            Self::ModelNotFound { .. } => Some("model_not_found"),
73            Self::InsufficientQuota { .. } => Some("insufficient_quota"),
74            Self::Network { .. } => Some("network_error"),
75            Self::Json { .. } => Some("json_error"),
76            Self::Config { .. } => Some("config_error"),
77            Self::Other { .. } => Some("other_error"),
78        }
79    }
80
81    fn is_retryable(&self) -> bool {
82        match self {
83            Self::RateLimit { .. } => true,
84            Self::ServiceUnavailable { .. } => true,
85            Self::Network { source } => {
86                // Retry on timeout and connection errors
87                source.is_timeout() || source.is_connect()
88            }
89            _ => false,
90        }
91    }
92
93    fn is_rate_limited(&self) -> bool {
94        matches!(self, Self::RateLimit { .. })
95    }
96
97    fn is_auth_error(&self) -> bool {
98        matches!(
99            self,
100            Self::Authentication { .. } | Self::InsufficientQuota { .. }
101        )
102    }
103
104    fn retry_after(&self) -> Option<Duration> {
105        match self {
106            Self::RateLimit { retry_after } => *retry_after,
107            _ => None,
108        }
109    }
110
111    fn is_invalid_input(&self) -> bool {
112        matches!(
113            self,
114            Self::InvalidRequest { .. } | Self::ModelNotFound { .. }
115        )
116    }
117
118    fn is_service_unavailable(&self) -> bool {
119        matches!(self, Self::ServiceUnavailable { .. })
120    }
121
122    fn is_content_filtered(&self) -> bool {
123        matches!(self, Self::ContentFiltered { .. })
124    }
125}
126
127impl OpenAIError {
128    /// Create an error from an HTTP status code and response body.
129    pub fn from_response(status: u16, body: &str) -> Self {
130        // Try to parse the error response
131        if let Ok(error_response) = serde_json::from_str::<OpenAIErrorResponse>(body) {
132            Self::from_error_response(status, error_response)
133        } else {
134            // Fallback to generic error based on status code
135            match status {
136                401 => Self::Authentication {
137                    message: "Invalid API key".to_string(),
138                },
139                403 => Self::Authentication {
140                    message: "Forbidden".to_string(),
141                },
142                429 => Self::RateLimit { retry_after: None },
143                400 => Self::InvalidRequest {
144                    message: body.to_string(),
145                },
146                404 => Self::InvalidRequest {
147                    message: "Not found".to_string(),
148                },
149                500..=599 => Self::ServiceUnavailable {
150                    message: format!("Server error: {status}"),
151                },
152                _ => Self::Other {
153                    message: format!("HTTP {status}: {body}"),
154                },
155            }
156        }
157    }
158
159    /// Create an error from a parsed OpenAI error response.
160    pub fn from_error_response(status: u16, response: OpenAIErrorResponse) -> Self {
161        let error = &response.error;
162
163        match error.error_type.as_deref() {
164            Some("invalid_api_key") => Self::Authentication {
165                message: error.message.clone(),
166            },
167            Some("insufficient_quota") => Self::InsufficientQuota {
168                message: error.message.clone(),
169            },
170            Some("model_not_found") => Self::ModelNotFound {
171                model: error.message.clone(),
172            },
173            Some("rate_limit_exceeded") => Self::RateLimit {
174                retry_after: None, // Could parse from headers
175            },
176            Some("content_filter") => Self::ContentFiltered {
177                message: error.message.clone(),
178            },
179            _ => match status {
180                400 => Self::InvalidRequest {
181                    message: error.message.clone(),
182                },
183                401 | 403 => Self::Authentication {
184                    message: error.message.clone(),
185                },
186                429 => Self::RateLimit { retry_after: None },
187                500..=599 => Self::ServiceUnavailable {
188                    message: error.message.clone(),
189                },
190                _ => Self::Other {
191                    message: error.message.clone(),
192                },
193            },
194        }
195    }
196}
197
198/// OpenAI API error response structure.
199#[derive(Debug, serde::Deserialize)]
200pub struct OpenAIErrorResponse {
201    pub error: OpenAIErrorDetail,
202}
203
204/// OpenAI API error detail.
205#[derive(Debug, serde::Deserialize)]
206pub struct OpenAIErrorDetail {
207    pub message: String,
208    #[serde(rename = "type")]
209    pub error_type: Option<String>,
210    pub param: Option<String>,
211    pub code: Option<String>,
212}