1use std::time::Duration;
4
5use thiserror::Error;
6
7use crate::error::BoxError;
8
9#[derive(Debug, Error)]
16#[error("{kind}")]
17pub struct ProviderError {
18 pub kind: ProviderErrorKind,
19 pub request_id: Option<String>,
23}
24
25impl ProviderError {
26 pub fn new(kind: ProviderErrorKind) -> Self {
27 Self {
28 kind,
29 request_id: None,
30 }
31 }
32
33 pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
34 self.request_id = Some(request_id.into());
35 self
36 }
37
38 pub fn retry_hint(&self) -> RetryHint {
40 use ProviderErrorKind::*;
41 match &self.kind {
42 AuthMissing { .. }
43 | AuthMalformed { .. }
44 | AuthRejected { .. }
45 | ModelNotFound { .. }
46 | BadRequest { .. }
47 | InvalidToolSchema { .. }
48 | InputBlocked { .. }
49 | OutputBlocked { .. }
50 | ProtocolViolation { .. }
51 | MaxTokensInvalid { .. }
52 | QuotaExceeded { .. }
53 | Canceled
54 | Other(_) => RetryHint::No,
55
56 AuthExpired => RetryHint::AfterAction(RetryAction::RefreshAuth),
57 ContextOverflow { .. } => RetryHint::AfterAction(RetryAction::ReduceContext),
58
59 RateLimit {
60 retry_after: Some(d),
61 ..
62 } => RetryHint::After(*d),
63 RateLimit {
64 retry_after: None, ..
65 } => RetryHint::Backoff,
66
67 ServerError { .. }
68 | ServerStreamAborted { .. }
69 | Malformed(_)
70 | Transport(_)
71 | Timeout { .. } => RetryHint::Backoff,
72 }
73 }
74
75 pub fn is_retryable(&self) -> bool {
77 !matches!(self.retry_hint(), RetryHint::No)
78 }
79}
80
81#[derive(Debug, Error)]
88pub enum ProviderErrorKind {
89 #[error("missing credential{}", var_hint.as_deref().map(|h| format!(" (hint: {h})")).unwrap_or_default())]
92 AuthMissing { var_hint: Option<String> },
93
94 #[error("malformed credential{}", hint.as_deref().map(|h| format!(": {h}")).unwrap_or_default())]
96 AuthMalformed { hint: Option<String> },
97
98 #[error("credential rejected by server{}", hint.as_deref().map(|h| format!(": {h}")).unwrap_or_default())]
100 AuthRejected { hint: Option<String> },
101
102 #[error("auth token expired")]
104 AuthExpired,
105
106 #[error("rate limit hit ({scope:?}){}", retry_after.map(|d| format!(", retry after {}s", d.as_secs())).unwrap_or_default())]
109 RateLimit {
110 retry_after: Option<Duration>,
111 scope: RateLimitScope,
112 },
113
114 #[error("quota exceeded{}", hint.as_deref().map(|h| format!(": {h}")).unwrap_or_default())]
116 QuotaExceeded { hint: Option<String> },
117
118 #[error("context overflow{}", match (used, limit) {
121 (Some(u), Some(l)) => format!(" ({u} > {l})"),
122 _ => String::new(),
123 })]
124 ContextOverflow {
125 used: Option<u64>,
126 limit: Option<u64>,
127 },
128
129 #[error("max_tokens invalid{}", match (requested, limit) {
132 (Some(r), Some(l)) => format!(" ({r} > {l})"),
133 _ => String::new(),
134 })]
135 MaxTokensInvalid {
136 requested: Option<u64>,
137 limit: Option<u64>,
138 },
139
140 #[error("model not found: {model}")]
142 ModelNotFound { model: String },
143
144 #[error("bad request{}", hint.as_deref().map(|h| format!(": {h}")).unwrap_or_default())]
147 BadRequest { hint: Option<String> },
148
149 #[error("invalid tool schema for {tool}{}", hint.as_deref().map(|h| format!(": {h}")).unwrap_or_default())]
151 InvalidToolSchema { tool: String, hint: Option<String> },
152
153 #[error("input blocked{}", policy.as_deref().map(|p| format!(" by {p}")).unwrap_or_default())]
156 InputBlocked { policy: Option<String> },
157
158 #[error("output blocked{}", policy.as_deref().map(|p| format!(" by {p}")).unwrap_or_default())]
160 OutputBlocked { policy: Option<String> },
161
162 #[error("server error{}{}",
165 status.map(|s| format!(" ({s})")).unwrap_or_default(),
166 hint.as_deref().map(|h| format!(": {h}")).unwrap_or_default())]
167 ServerError {
168 status: Option<u16>,
169 hint: Option<String>,
170 },
171
172 #[error("server aborted stream{}", hint.as_deref().map(|h| format!(": {h}")).unwrap_or_default())]
174 ServerStreamAborted { hint: Option<String> },
175
176 #[error("malformed wire response: {0}")]
178 Malformed(#[source] BoxError),
179
180 #[error("protocol violation: {hint}")]
183 ProtocolViolation { hint: String },
184
185 #[error("transport error: {0}")]
188 Transport(#[source] BoxError),
189
190 #[error("request timeout at {phase:?}")]
192 Timeout { phase: TimeoutPhase },
193
194 #[error("canceled")]
197 Canceled,
198
199 #[error("other provider error: {0}")]
202 Other(#[source] BoxError),
203}
204
205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
206pub enum RateLimitScope {
207 Rpm,
209 Tpm,
211 Unspecified,
213}
214
215#[derive(Debug, Clone, Copy, PartialEq, Eq)]
216pub enum TimeoutPhase {
217 Connect,
218 ReadHeaders,
219 ReadBody,
220 Idle,
221 Total,
222}
223
224#[derive(Debug, Clone, Copy, PartialEq, Eq)]
226pub enum RetryHint {
227 No,
229 Immediate,
231 After(Duration),
233 Backoff,
235 AfterAction(RetryAction),
237}
238
239#[derive(Debug, Clone, Copy, PartialEq, Eq)]
240pub enum RetryAction {
241 RefreshAuth,
242 SwitchModel,
243 ReduceContext,
244}