1use thiserror::Error;
19
20#[derive(Debug, Error)]
29pub enum OpenAIError {
30 #[error("Configuration Error: {0}")]
32 ConfigError(String),
33
34 #[error("HTTP Error: {0}")]
38 HTTPError(#[from] reqwest::Error),
39
40 #[error("Deserialization/Parsing Error: {0}")]
44 DeserializeError(#[from] serde_json::Error),
45
46 #[error("OpenAI API Error: {message}")]
52 APIError {
53 message: String,
55 #[allow(dead_code)]
57 err_type: Option<String>,
58 #[allow(dead_code)]
60 code: Option<String>,
61 },
62
63 #[error("IO Error: {0}")]
65 IOError(#[from] std::io::Error),
66}
67
68impl OpenAIError {
69 pub fn api_error(
85 message: impl Into<String>,
86 err_type: Option<&str>,
87 code: Option<&str>,
88 ) -> Self {
89 OpenAIError::APIError {
90 message: message.into(),
91 err_type: err_type.map(|s| s.to_string()),
92 code: code.map(|s| s.to_string()),
93 }
94 }
95}
96
97#[derive(Debug, serde::Deserialize)]
103pub(crate) struct OpenAIAPIErrorBody {
104 pub error: OpenAIAPIErrorDetails,
106}
107
108#[derive(Debug, serde::Deserialize)]
110pub(crate) struct OpenAIAPIErrorDetails {
111 pub message: String,
113 #[serde(rename = "type")]
115 pub err_type: String,
116 pub code: Option<String>,
118}
119
120impl From<OpenAIAPIErrorBody> for OpenAIError {
121 fn from(body: OpenAIAPIErrorBody) -> Self {
122 OpenAIError::APIError {
123 message: body.error.message,
124 err_type: Some(body.error.err_type),
125 code: body.error.code,
126 }
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
139 use std::fmt::Write as _;
140
141 fn produce_reqwest_error() -> reqwest::Error {
144 reqwest::blocking::Client::new()
146 .get("http://this-domain-should-not-exist9999.test")
147 .send()
148 .unwrap_err()
149 }
150
151 fn produce_serde_json_error() -> serde_json::Error {
153 serde_json::from_str::<serde_json::Value>("\"unterminated string").unwrap_err()
154 }
155
156 fn produce_io_error() -> std::io::Error {
158 std::fs::File::open("non_existent_file.txt").unwrap_err()
159 }
160
161 #[test]
162 fn test_config_error() {
163 let err = OpenAIError::ConfigError("No API key found".to_string());
164 let display_str = format!("{}", err);
165
166 match &err {
168 OpenAIError::ConfigError(msg) => {
169 assert_eq!(msg, "No API key found");
170 }
171 other => panic!("Expected ConfigError, got: {:?}", other),
172 }
173
174 assert!(
176 display_str.contains("No API key found"),
177 "Display should contain the config error message, got: {}",
178 display_str
179 );
180 }
181
182 #[test]
183 fn test_http_error() {
184 let reqwest_err = produce_reqwest_error();
185 let err = OpenAIError::HTTPError(reqwest_err);
186
187 let display_str = format!("{}", err);
188 assert!(
189 display_str.contains("HTTP Error:"),
190 "Should contain 'HTTP Error:' prefix, got: {}",
191 display_str
192 );
193
194 match &err {
196 OpenAIError::HTTPError(e) => {
197 let e_str = format!("{}", e);
198 assert!(
200 e_str.contains("error sending request")
201 || e_str.contains("dns error")
202 || e_str.contains("Could not resolve host")
203 || e_str.contains("Name or service not known"),
204 "Expected mention of DNS/resolve error or sending request, got: {}",
205 e_str
206 );
207 }
208 other => panic!("Expected HTTPError, got: {:?}", other),
209 }
210 }
211
212 #[test]
213 fn test_deserialize_error() {
214 let serde_err = produce_serde_json_error();
216 let err = OpenAIError::DeserializeError(serde_err);
217
218 let display_str = format!("{}", err);
220 assert!(
221 display_str.contains("Deserialization/Parsing Error:"),
222 "Should contain 'Deserialization/Parsing Error:', got: {}",
223 display_str
224 );
225
226 match &err {
228 OpenAIError::DeserializeError(e) => {
229 let e_str = format!("{}", e);
230 assert!(
231 e_str.contains("EOF while parsing a string")
232 || e_str.contains("unterminated string"),
233 "Expected mention of parse error about unterminated, got: {}",
234 e_str
235 );
236 }
237 other => panic!("Expected DeserializeError, got: {:?}", other),
238 }
239 }
240
241 #[test]
242 fn test_api_error() {
243 let err = OpenAIError::api_error(
245 "Something went wrong",
246 Some("invalid_request_error"),
247 Some("ERR123"),
248 );
249 let display_str = format!("{}", err);
250
251 match &err {
252 OpenAIError::APIError {
253 message,
254 err_type,
255 code,
256 } => {
257 assert_eq!(message, "Something went wrong");
258 assert_eq!(err_type.as_deref(), Some("invalid_request_error"));
259 assert_eq!(code.as_deref(), Some("ERR123"));
260 }
261 other => panic!("Expected APIError, got: {:?}", other),
262 }
263
264 assert!(
266 display_str.contains("OpenAI API Error: Something went wrong"),
267 "Expected 'OpenAI API Error:' prefix, got: {}",
268 display_str
269 );
270 }
271
272 #[test]
273 fn test_from_openaiapierrorbody() {
274 let body = OpenAIAPIErrorBody {
275 error: OpenAIAPIErrorDetails {
276 message: "Rate limit exceeded".to_string(),
277 err_type: "rate_limit_error".to_string(),
278 code: Some("rate_limit_code".to_string()),
279 },
280 };
281 let err = OpenAIError::from(body);
282
283 match &err {
284 OpenAIError::APIError {
285 message,
286 err_type,
287 code,
288 } => {
289 assert_eq!(message, "Rate limit exceeded");
290 assert_eq!(err_type.as_deref(), Some("rate_limit_error"));
291 assert_eq!(code.as_deref(), Some("rate_limit_code"));
292 }
293 other => panic!("Expected APIError from error body, got: {:?}", other),
294 }
295 }
296
297 #[test]
298 fn test_io_error() {
299 let io_err = produce_io_error();
300 let err: OpenAIError = io_err.into();
301
302 let display_str = format!("{}", err);
303 assert!(
304 display_str.contains("IO Error:"),
305 "Display should contain 'IO Error:' prefix, got: {}",
306 display_str
307 );
308
309 match &err {
311 OpenAIError::IOError(e) => {
312 let e_str = format!("{}", e);
313 let lower = e_str.to_lowercase();
314 assert!(
315 lower.contains("no such file")
316 || lower.contains("not found")
317 || lower.contains("os error 2"),
318 "Expected mention of file not found error, got: {}",
319 e_str
320 );
321 }
322 other => panic!("Expected IOError, got: {:?}", other),
323 }
324 }
325
326 #[test]
327 fn test_display_trait_all_variants() {
328 let config_err = OpenAIError::ConfigError("missing key".to_string());
329 let http_err = OpenAIError::HTTPError(produce_reqwest_error());
330 let deser_err = OpenAIError::DeserializeError(produce_serde_json_error());
331 let api_err = OpenAIError::api_error("Remote server said no", Some("some_api_error"), None);
332 let io_err = OpenAIError::IOError(produce_io_error());
333
334 let mut combined = String::new();
335 writeln!(&mut combined, "{}", config_err).unwrap();
336 writeln!(&mut combined, "{}", http_err).unwrap();
337 writeln!(&mut combined, "{}", deser_err).unwrap();
338 writeln!(&mut combined, "{}", api_err).unwrap();
339 writeln!(&mut combined, "{}", io_err).unwrap();
340
341 assert!(combined.contains("Configuration Error: missing key"));
343 assert!(combined.contains("HTTP Error:"));
344 assert!(combined.contains("Deserialization/Parsing Error:"));
345 assert!(combined.contains("OpenAI API Error: Remote server said no"));
346 assert!(combined.contains("IO Error:"));
347 }
348}