ferrous_llm_openai/
error.rs1use ferrous_llm_core::ProviderError;
4use std::time::Duration;
5use thiserror::Error;
6
7#[derive(Debug, Error)]
9pub enum OpenAIError {
10 #[error("Authentication failed: {message}")]
12 Authentication { message: String },
13
14 #[error("Rate limited: retry after {retry_after:?}")]
16 RateLimit { retry_after: Option<Duration> },
17
18 #[error("Invalid request: {message}")]
20 InvalidRequest { message: String },
21
22 #[error("Service unavailable: {message}")]
24 ServiceUnavailable { message: String },
25
26 #[error("Content filtered: {message}")]
28 ContentFiltered { message: String },
29
30 #[error("Model not found: {model}")]
32 ModelNotFound { model: String },
33
34 #[error("Insufficient quota: {message}")]
36 InsufficientQuota { message: String },
37
38 #[error("Network error: {source}")]
40 Network {
41 #[from]
42 source: reqwest::Error,
43 },
44
45 #[error("JSON parsing error: {source}")]
47 Json {
48 #[from]
49 source: serde_json::Error,
50 },
51
52 #[error("Configuration error: {source}")]
54 Config {
55 #[from]
56 source: ferrous_llm_core::ConfigError,
57 },
58
59 #[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 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 pub fn from_response(status: u16, body: &str) -> Self {
130 if let Ok(error_response) = serde_json::from_str::<OpenAIErrorResponse>(body) {
132 Self::from_error_response(status, error_response)
133 } else {
134 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 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, },
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#[derive(Debug, serde::Deserialize)]
200pub struct OpenAIErrorResponse {
201 pub error: OpenAIErrorDetail,
202}
203
204#[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}