1use std::backtrace::Backtrace;
13use std::collections::HashMap;
14use std::fmt;
15
16use serde_json::Value as JsonValue;
17
18pub type Result<T> = std::result::Result<T, ProviderError>;
20
21type Source = Box<dyn std::error::Error + Send + Sync + 'static>;
23
24pub struct ProviderError {
43 inner: Box<ErrorInner>,
44}
45
46struct ErrorInner {
47 kind: ErrorKind,
48 backtrace: Backtrace,
49 source: Option<Source>,
50}
51
52#[derive(Debug)]
58#[expect(
59 dead_code,
60 reason = "value/text retained for Debug output and future accessors"
61)]
62pub(crate) enum ErrorKind {
63 ApiCall(ApiCallData),
64 InvalidArgument {
65 argument: String,
66 message: String,
67 },
68 InvalidPrompt {
69 message: String,
70 },
71 TypeValidation {
72 path: String,
73 value: JsonValue,
74 message: String,
75 },
76 JsonParse {
77 text: String,
78 message: String,
79 },
80 EmptyResponseBody,
81 NoContentGenerated,
82 NoSuchModel {
83 model_id: String,
84 model_type: String,
85 },
86 Unsupported {
87 functionality: String,
88 },
89 LoadApiKey {
90 message: String,
91 },
92 TooManyEmbeddingValues {
93 max: usize,
94 actual: usize,
95 },
96}
97
98#[derive(Debug, Default)]
100pub(crate) struct ApiCallData {
101 pub url: String,
102 pub message: String,
103 pub status_code: Option<u16>,
104 pub response_headers: Option<HashMap<String, String>>,
105 pub response_body: Option<String>,
106 pub request_body: Option<JsonValue>,
107 pub is_retryable: bool,
108}
109
110impl ProviderError {
111 pub fn api_call(url: impl Into<String>, message: impl Into<String>) -> Self {
119 Self::from_kind(ErrorKind::ApiCall(ApiCallData {
120 url: url.into(),
121 message: message.into(),
122 ..ApiCallData::default()
123 }))
124 }
125
126 pub fn api_call_builder(
128 url: impl Into<String>,
129 message: impl Into<String>,
130 ) -> ApiCallErrorBuilder {
131 ApiCallErrorBuilder {
132 data: ApiCallData {
133 url: url.into(),
134 message: message.into(),
135 ..ApiCallData::default()
136 },
137 source: None,
138 retryable_override: None,
139 }
140 }
141
142 pub fn invalid_argument(argument: impl Into<String>, message: impl Into<String>) -> Self {
144 Self::from_kind(ErrorKind::InvalidArgument {
145 argument: argument.into(),
146 message: message.into(),
147 })
148 }
149
150 pub fn invalid_prompt(message: impl Into<String>) -> Self {
152 Self::from_kind(ErrorKind::InvalidPrompt {
153 message: message.into(),
154 })
155 }
156
157 pub fn type_validation(
159 path: impl Into<String>,
160 value: JsonValue,
161 message: impl Into<String>,
162 ) -> Self {
163 Self::from_kind(ErrorKind::TypeValidation {
164 path: path.into(),
165 value,
166 message: message.into(),
167 })
168 }
169
170 pub fn json_parse(text: impl Into<String>, message: impl Into<String>) -> Self {
172 Self::from_kind(ErrorKind::JsonParse {
173 text: text.into(),
174 message: message.into(),
175 })
176 }
177
178 #[must_use]
180 pub fn empty_response_body() -> Self {
181 Self::from_kind(ErrorKind::EmptyResponseBody)
182 }
183
184 #[must_use]
186 pub fn no_content_generated() -> Self {
187 Self::from_kind(ErrorKind::NoContentGenerated)
188 }
189
190 pub fn no_such_model(model_id: impl Into<String>, model_type: impl Into<String>) -> Self {
192 Self::from_kind(ErrorKind::NoSuchModel {
193 model_id: model_id.into(),
194 model_type: model_type.into(),
195 })
196 }
197
198 pub fn unsupported(functionality: impl Into<String>) -> Self {
200 Self::from_kind(ErrorKind::Unsupported {
201 functionality: functionality.into(),
202 })
203 }
204
205 pub fn load_api_key(message: impl Into<String>) -> Self {
207 Self::from_kind(ErrorKind::LoadApiKey {
208 message: message.into(),
209 })
210 }
211
212 #[must_use]
214 pub fn too_many_embedding_values(max: usize, actual: usize) -> Self {
215 Self::from_kind(ErrorKind::TooManyEmbeddingValues { max, actual })
216 }
217
218 #[must_use]
222 pub fn is_api_call(&self) -> bool {
223 matches!(self.inner.kind, ErrorKind::ApiCall(_))
224 }
225
226 #[must_use]
231 pub fn is_retryable(&self) -> bool {
232 matches!(&self.inner.kind, ErrorKind::ApiCall(d) if d.is_retryable)
233 }
234
235 #[must_use]
237 pub fn is_no_such_model(&self) -> bool {
238 matches!(self.inner.kind, ErrorKind::NoSuchModel { .. })
239 }
240
241 #[must_use]
243 pub fn is_unsupported(&self) -> bool {
244 matches!(self.inner.kind, ErrorKind::Unsupported { .. })
245 }
246
247 #[must_use]
249 pub fn status_code(&self) -> Option<u16> {
250 match &self.inner.kind {
251 ErrorKind::ApiCall(d) => d.status_code,
252 _ => None,
253 }
254 }
255
256 #[must_use]
261 pub fn response_body(&self) -> Option<&str> {
262 match &self.inner.kind {
263 ErrorKind::ApiCall(d) => d.response_body.as_deref(),
264 _ => None,
265 }
266 }
267
268 #[must_use]
270 pub fn url(&self) -> Option<&str> {
271 match &self.inner.kind {
272 ErrorKind::ApiCall(d) => Some(&d.url),
273 _ => None,
274 }
275 }
276
277 #[must_use]
279 pub fn model_id(&self) -> Option<&str> {
280 match &self.inner.kind {
281 ErrorKind::NoSuchModel { model_id, .. } => Some(model_id),
282 _ => None,
283 }
284 }
285
286 pub fn backtrace(&self) -> &Backtrace {
288 &self.inner.backtrace
289 }
290
291 fn from_kind(kind: ErrorKind) -> Self {
294 Self {
295 inner: Box::new(ErrorInner {
296 kind,
297 backtrace: Backtrace::capture(),
298 source: None,
299 }),
300 }
301 }
302
303 pub(crate) fn with_source(mut self, source: Source) -> Self {
304 self.inner.source = Some(source);
305 self
306 }
307}
308
309#[derive(Debug)]
314pub struct ApiCallErrorBuilder {
315 data: ApiCallData,
316 source: Option<Source>,
317 retryable_override: Option<bool>,
318}
319
320impl ApiCallErrorBuilder {
321 #[must_use]
323 pub fn status_code(mut self, code: u16) -> Self {
324 self.data.status_code = Some(code);
325 self
326 }
327
328 #[must_use]
330 pub fn response_body(mut self, body: impl Into<String>) -> Self {
331 self.data.response_body = Some(body.into());
332 self
333 }
334
335 #[must_use]
337 pub fn response_headers(mut self, headers: HashMap<String, String>) -> Self {
338 self.data.response_headers = Some(headers);
339 self
340 }
341
342 #[must_use]
344 pub fn request_body(mut self, body: JsonValue) -> Self {
345 self.data.request_body = Some(body);
346 self
347 }
348
349 #[must_use]
354 pub fn retryable(mut self, retryable: bool) -> Self {
355 self.retryable_override = Some(retryable);
356 self
357 }
358
359 #[must_use]
361 pub fn source<E>(mut self, err: E) -> Self
362 where
363 E: std::error::Error + Send + Sync + 'static,
364 {
365 self.source = Some(Box::new(err));
366 self
367 }
368
369 #[must_use]
371 pub fn build(mut self) -> ProviderError {
372 let derived = matches!(self.data.status_code, Some(408 | 409 | 429 | 500..));
374 self.data.is_retryable = self.retryable_override.unwrap_or(derived);
375 let err = ProviderError::from_kind(ErrorKind::ApiCall(self.data));
376 if let Some(src) = self.source {
377 err.with_source(src)
378 } else {
379 err
380 }
381 }
382}
383
384impl fmt::Debug for ProviderError {
387 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
388 f.debug_struct("ProviderError")
389 .field("kind", &self.inner.kind)
390 .field("source", &self.inner.source)
391 .finish_non_exhaustive()
392 }
393}
394
395impl fmt::Display for ProviderError {
396 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
397 match &self.inner.kind {
398 ErrorKind::ApiCall(d) => {
399 write!(f, "api call to {} failed: {}", d.url, d.message)?;
400 if let Some(code) = d.status_code {
401 write!(f, " (status {code})")?;
402 }
403 Ok(())
404 }
405 ErrorKind::InvalidArgument { argument, message } => {
406 write!(f, "invalid argument `{argument}`: {message}")
407 }
408 ErrorKind::InvalidPrompt { message } => write!(f, "invalid prompt: {message}"),
409 ErrorKind::TypeValidation { path, message, .. } => {
410 write!(f, "type validation failed at `{path}`: {message}")
411 }
412 ErrorKind::JsonParse { message, .. } => write!(f, "json parse error: {message}"),
413 ErrorKind::EmptyResponseBody => f.write_str("empty response body"),
414 ErrorKind::NoContentGenerated => f.write_str("no content generated"),
415 ErrorKind::NoSuchModel {
416 model_id,
417 model_type,
418 } => {
419 write!(f, "no such {model_type}: `{model_id}`")
420 }
421 ErrorKind::Unsupported { functionality } => {
422 write!(f, "unsupported functionality: {functionality}")
423 }
424 ErrorKind::LoadApiKey { message } => write!(f, "could not load api key: {message}"),
425 ErrorKind::TooManyEmbeddingValues { max, actual } => {
426 write!(f, "too many embedding values: max {max}, got {actual}")
427 }
428 }
429 }
430}
431
432impl std::error::Error for ProviderError {
433 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
434 self.inner
435 .source
436 .as_deref()
437 .map(|e| e as &(dyn std::error::Error + 'static))
438 }
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444
445 #[test]
446 fn helpers_branch_correctly() {
447 let e = ProviderError::no_such_model("gpt-foo", "languageModel");
448 assert!(e.is_no_such_model());
449 assert_eq!(e.model_id(), Some("gpt-foo"));
450 assert!(!e.is_retryable());
451 }
452
453 #[test]
454 fn api_call_builder_auto_retryable() {
455 let e = ProviderError::api_call_builder("https://api.test", "boom")
456 .status_code(503)
457 .build();
458 assert!(e.is_api_call());
459 assert!(e.is_retryable());
460 assert_eq!(e.status_code(), Some(503));
461 }
462
463 #[test]
464 fn api_call_builder_explicit_non_retryable() {
465 let e = ProviderError::api_call_builder("https://api.test", "boom")
466 .status_code(500)
467 .retryable(false)
468 .build();
469 assert!(!e.is_retryable());
470 }
471
472 #[test]
473 fn display_format_stable() {
474 let e = ProviderError::invalid_argument("temperature", "must be >= 0");
475 assert_eq!(
476 format!("{e}"),
477 "invalid argument `temperature`: must be >= 0"
478 );
479 }
480}