1use thiserror::Error;
5
6#[derive(Debug, Error)]
7pub enum ProviderError {
8 #[error("unauthorized: {0}")]
9 Unauthorized(String),
10
11 #[error("rate limited (retry after {retry_after_ms} ms)")]
12 RateLimited { retry_after_ms: u64 },
13
14 #[error("model not found: {model}")]
15 ModelNotFound { model: String },
16
17 #[error("invalid request: {0}")]
18 InvalidRequest(String),
19
20 #[error("upstream provider error (status {status}): {message}")]
21 ProviderUpstream { status: u16, message: String },
22
23 #[error("timeout after {ms} ms")]
24 Timeout { ms: u64 },
25
26 #[error("network error: {0}")]
27 Network(#[from] reqwest::Error),
28
29 #[error("deserialize error: {0}")]
30 Deserialize(String),
31
32 #[error("unsupported feature: {0}")]
33 Unsupported(String),
34
35 #[error("internal error: {0}")]
36 Internal(String),
37}
38
39impl ProviderError {
40 pub fn is_retriable(&self) -> bool {
42 match self {
43 ProviderError::RateLimited { .. } => true,
44 ProviderError::Timeout { .. } => true,
45 ProviderError::Network(_) => true,
46 ProviderError::ProviderUpstream { status, .. } => *status >= 500,
47 _ => false,
48 }
49 }
50
51 pub fn is_fallback_eligible(&self) -> bool {
60 match self {
61 ProviderError::ModelNotFound { .. } => true,
62 ProviderError::Timeout { .. } => true,
63 ProviderError::ProviderUpstream { status, .. } => *status >= 500,
64 _ => false,
65 }
66 }
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72
73 #[test]
74 fn upstream_5xx_is_fallback_eligible() {
75 assert!(ProviderError::ProviderUpstream {
76 status: 500,
77 message: "boom".into()
78 }
79 .is_fallback_eligible());
80 assert!(ProviderError::ProviderUpstream {
81 status: 503,
82 message: "unavailable".into()
83 }
84 .is_fallback_eligible());
85 }
86
87 #[test]
88 fn upstream_4xx_is_not_fallback_eligible() {
89 for status in [400u16, 403, 404, 422] {
90 assert!(
91 !ProviderError::ProviderUpstream {
92 status,
93 message: "client error".into()
94 }
95 .is_fallback_eligible(),
96 "status {status} must not fail over"
97 );
98 }
99 }
100
101 #[test]
102 fn model_not_found_and_timeout_still_fallback_eligible() {
103 assert!(ProviderError::ModelNotFound { model: "x".into() }.is_fallback_eligible());
104 assert!(ProviderError::Timeout { ms: 1000 }.is_fallback_eligible());
105 }
106
107 #[test]
108 fn invalid_request_and_unauthorized_not_fallback_eligible() {
109 assert!(!ProviderError::InvalidRequest("bad".into()).is_fallback_eligible());
110 assert!(!ProviderError::Unauthorized("nope".into()).is_fallback_eligible());
111 }
112}