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#[non_exhaustive]
88#[derive(Debug, Error)]
89pub enum ProviderErrorKind {
90 #[error("missing credential{}", var_hint.as_deref().map(|h| format!(" (hint: {h})")).unwrap_or_default())]
93 AuthMissing { var_hint: Option<String> },
94
95 #[error("malformed credential{}", hint.as_deref().map(|h| format!(": {h}")).unwrap_or_default())]
97 AuthMalformed { hint: Option<String> },
98
99 #[error("credential rejected by server{}", hint.as_deref().map(|h| format!(": {h}")).unwrap_or_default())]
101 AuthRejected { hint: Option<String> },
102
103 #[error("auth token expired")]
105 AuthExpired,
106
107 #[error("rate limit hit ({scope:?}){}", retry_after.map(|d| format!(", retry after {}s", d.as_secs())).unwrap_or_default())]
110 RateLimit {
111 retry_after: Option<Duration>,
112 scope: RateLimitScope,
113 },
114
115 #[error("quota exceeded{}", hint.as_deref().map(|h| format!(": {h}")).unwrap_or_default())]
117 QuotaExceeded { hint: Option<String> },
118
119 #[error("context overflow{}", match (used, limit) {
122 (Some(u), Some(l)) => format!(" ({u} > {l})"),
123 _ => String::new(),
124 })]
125 ContextOverflow {
126 used: Option<u64>,
127 limit: Option<u64>,
128 },
129
130 #[error("max_tokens invalid{}", match (requested, limit) {
133 (Some(r), Some(l)) => format!(" ({r} > {l})"),
134 _ => String::new(),
135 })]
136 MaxTokensInvalid {
137 requested: Option<u64>,
138 limit: Option<u64>,
139 },
140
141 #[error("model not found: {model}")]
143 ModelNotFound { model: String },
144
145 #[error("bad request{}", hint.as_deref().map(|h| format!(": {h}")).unwrap_or_default())]
148 BadRequest { hint: Option<String> },
149
150 #[error("invalid tool schema for {tool}{}", hint.as_deref().map(|h| format!(": {h}")).unwrap_or_default())]
152 InvalidToolSchema { tool: String, hint: Option<String> },
153
154 #[error("input blocked{}", policy.as_deref().map(|p| format!(" by {p}")).unwrap_or_default())]
157 InputBlocked { policy: Option<String> },
158
159 #[error("output blocked{}", policy.as_deref().map(|p| format!(" by {p}")).unwrap_or_default())]
161 OutputBlocked { policy: Option<String> },
162
163 #[error("server error{}{}",
166 status.map(|s| format!(" ({s})")).unwrap_or_default(),
167 hint.as_deref().map(|h| format!(": {h}")).unwrap_or_default())]
168 ServerError {
169 status: Option<u16>,
170 hint: Option<String>,
171 },
172
173 #[error("server aborted stream{}", hint.as_deref().map(|h| format!(": {h}")).unwrap_or_default())]
175 ServerStreamAborted { hint: Option<String> },
176
177 #[error("malformed wire response: {0}")]
179 Malformed(#[source] BoxError),
180
181 #[error("protocol violation: {hint}")]
184 ProtocolViolation { hint: String },
185
186 #[error("transport error: {0}")]
189 Transport(#[source] BoxError),
190
191 #[error("request timeout at {phase:?}")]
193 Timeout { phase: TimeoutPhase },
194
195 #[error("canceled")]
198 Canceled,
199
200 #[error("other provider error: {0}")]
203 Other(#[source] BoxError),
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq)]
207pub enum RateLimitScope {
208 Rpm,
210 Tpm,
212 Unspecified,
214}
215
216#[derive(Debug, Clone, Copy, PartialEq, Eq)]
217pub enum TimeoutPhase {
218 Connect,
219 ReadHeaders,
220 ReadBody,
221 Idle,
222 Total,
223}
224
225#[derive(Debug, Clone, Copy, PartialEq, Eq)]
227pub enum RetryHint {
228 No,
230 Immediate,
232 After(Duration),
234 Backoff,
236 AfterAction(RetryAction),
238}
239
240#[derive(Debug, Clone, Copy, PartialEq, Eq)]
241pub enum RetryAction {
242 RefreshAuth,
243 SwitchModel,
244 ReduceContext,
245}