use std::time::Duration;
use thiserror::Error;
use crate::error::BoxError;
#[derive(Debug, Error)]
#[error("{kind}")]
pub struct ProviderError {
pub kind: ProviderErrorKind,
pub request_id: Option<String>,
}
impl ProviderError {
pub fn new(kind: ProviderErrorKind) -> Self {
Self {
kind,
request_id: None,
}
}
pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
self.request_id = Some(request_id.into());
self
}
pub fn retry_hint(&self) -> RetryHint {
use ProviderErrorKind::*;
match &self.kind {
AuthMissing { .. }
| AuthMalformed { .. }
| AuthRejected { .. }
| ModelNotFound { .. }
| BadRequest { .. }
| InvalidToolSchema { .. }
| InputBlocked { .. }
| OutputBlocked { .. }
| ProtocolViolation { .. }
| MaxTokensInvalid { .. }
| QuotaExceeded { .. }
| Canceled
| Other(_) => RetryHint::No,
AuthExpired => RetryHint::AfterAction(RetryAction::RefreshAuth),
ContextOverflow { .. } => RetryHint::AfterAction(RetryAction::ReduceContext),
RateLimit {
retry_after: Some(d),
..
} => RetryHint::After(*d),
RateLimit {
retry_after: None, ..
} => RetryHint::Backoff,
ServerError { .. }
| ServerStreamAborted { .. }
| Malformed(_)
| Transport(_)
| Timeout { .. } => RetryHint::Backoff,
}
}
pub fn is_retryable(&self) -> bool {
!matches!(self.retry_hint(), RetryHint::No)
}
}
#[derive(Debug, Error)]
pub enum ProviderErrorKind {
#[error("missing credential{}", var_hint.as_deref().map(|h| format!(" (hint: {h})")).unwrap_or_default())]
AuthMissing { var_hint: Option<String> },
#[error("malformed credential{}", hint.as_deref().map(|h| format!(": {h}")).unwrap_or_default())]
AuthMalformed { hint: Option<String> },
#[error("credential rejected by server{}", hint.as_deref().map(|h| format!(": {h}")).unwrap_or_default())]
AuthRejected { hint: Option<String> },
#[error("auth token expired")]
AuthExpired,
#[error("rate limit hit ({scope:?}){}", retry_after.map(|d| format!(", retry after {}s", d.as_secs())).unwrap_or_default())]
RateLimit {
retry_after: Option<Duration>,
scope: RateLimitScope,
},
#[error("quota exceeded{}", hint.as_deref().map(|h| format!(": {h}")).unwrap_or_default())]
QuotaExceeded { hint: Option<String> },
#[error("context overflow{}", match (used, limit) {
(Some(u), Some(l)) => format!(" ({u} > {l})"),
_ => String::new(),
})]
ContextOverflow {
used: Option<u64>,
limit: Option<u64>,
},
#[error("max_tokens invalid{}", match (requested, limit) {
(Some(r), Some(l)) => format!(" ({r} > {l})"),
_ => String::new(),
})]
MaxTokensInvalid {
requested: Option<u64>,
limit: Option<u64>,
},
#[error("model not found: {model}")]
ModelNotFound { model: String },
#[error("bad request{}", hint.as_deref().map(|h| format!(": {h}")).unwrap_or_default())]
BadRequest { hint: Option<String> },
#[error("invalid tool schema for {tool}{}", hint.as_deref().map(|h| format!(": {h}")).unwrap_or_default())]
InvalidToolSchema { tool: String, hint: Option<String> },
#[error("input blocked{}", policy.as_deref().map(|p| format!(" by {p}")).unwrap_or_default())]
InputBlocked { policy: Option<String> },
#[error("output blocked{}", policy.as_deref().map(|p| format!(" by {p}")).unwrap_or_default())]
OutputBlocked { policy: Option<String> },
#[error("server error{}{}",
status.map(|s| format!(" ({s})")).unwrap_or_default(),
hint.as_deref().map(|h| format!(": {h}")).unwrap_or_default())]
ServerError {
status: Option<u16>,
hint: Option<String>,
},
#[error("server aborted stream{}", hint.as_deref().map(|h| format!(": {h}")).unwrap_or_default())]
ServerStreamAborted { hint: Option<String> },
#[error("malformed wire response: {0}")]
Malformed(#[source] BoxError),
#[error("protocol violation: {hint}")]
ProtocolViolation { hint: String },
#[error("transport error: {0}")]
Transport(#[source] BoxError),
#[error("request timeout at {phase:?}")]
Timeout { phase: TimeoutPhase },
#[error("canceled")]
Canceled,
#[error("other provider error: {0}")]
Other(#[source] BoxError),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RateLimitScope {
Rpm,
Tpm,
Unspecified,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TimeoutPhase {
Connect,
ReadHeaders,
ReadBody,
Idle,
Total,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RetryHint {
No,
Immediate,
After(Duration),
Backoff,
AfterAction(RetryAction),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RetryAction {
RefreshAuth,
SwitchModel,
ReduceContext,
}