1use serde::{Deserialize, Serialize};
4use thiserror::Error;
5use ts_rs::TS;
6
7#[derive(Debug, Clone, Serialize, Deserialize, TS, Error)]
9#[ts(export)]
10pub enum AiMuxError {
11 #[error("provider error: {0}")]
12 Provider(String),
13
14 #[error("HTTP error: {0}")]
15 Http(String),
16
17 #[error("JSON error: {0}")]
18 Json(String),
19
20 #[error("stream error: {0}")]
21 Stream(String),
22
23 #[error("tool error: {0}")]
24 Tool(String),
25
26 #[error("invalid argument: {0}")]
27 InvalidArgument(String),
28
29 #[error("invalid prompt: {0}")]
30 InvalidPrompt(String),
31
32 #[error("rate limited: retry after {retry_after_ms}ms")]
33 RateLimited { retry_after_ms: u64 },
34
35 #[error("authentication failed: {0}")]
36 Auth(String),
37
38 #[error("token expired: {0}")]
43 TokenExpired(String),
44
45 #[error("model not found: {0}")]
46 ModelNotFound(String),
47
48 #[error("unsupported functionality: {0}")]
49 Unsupported(String),
50
51 #[error("no such model: {0}")]
52 NoSuchModel(String),
53
54 #[error("unknown provider: {0}")]
55 UnknownProvider(String),
56
57 #[error("API call error: {0}")]
58 ApiCall(String),
59
60 #[error("request timed out: {0}")]
61 Timeout(String),
62
63 #[error("request aborted")]
64 Aborted,
65
66 #[error("{0}")]
67 Other(String),
68}
69
70impl From<serde_json::Error> for AiMuxError {
71 fn from(e: serde_json::Error) -> Self {
72 AiMuxError::Json(e.to_string())
73 }
74}
75
76impl AiMuxError {
77 pub fn is_retryable(&self) -> bool {
79 matches!(
80 self,
81 AiMuxError::RateLimited { .. } | AiMuxError::Http(_) | AiMuxError::ApiCall(_)
82 )
83 }
84
85 pub fn retry_after_hint(&self) -> Option<i64> {
94 match self {
95 AiMuxError::RateLimited { retry_after_ms } => Some(*retry_after_ms as i64),
96 _ => None,
97 }
98 }
99
100 pub fn error_type(&self) -> &'static str {
104 match self {
105 AiMuxError::Provider(_) => "Provider",
106 AiMuxError::Http(_) => "Http",
107 AiMuxError::Json(_) => "Json",
108 AiMuxError::Stream(_) => "Stream",
109 AiMuxError::Tool(_) => "Tool",
110 AiMuxError::InvalidArgument(_) => "InvalidArgument",
111 AiMuxError::InvalidPrompt(_) => "InvalidPrompt",
112 AiMuxError::RateLimited { .. } => "RateLimited",
113 AiMuxError::Auth(_) => "Auth",
114 AiMuxError::TokenExpired(_) => "TokenExpired",
115 AiMuxError::ModelNotFound(_) => "ModelNotFound",
116 AiMuxError::Unsupported(_) => "Unsupported",
117 AiMuxError::NoSuchModel(_) => "NoSuchModel",
118 AiMuxError::UnknownProvider(_) => "UnknownProvider",
119 AiMuxError::ApiCall(_) => "ApiCall",
120 AiMuxError::Timeout(_) => "Timeout",
121 AiMuxError::Aborted => "Aborted",
122 AiMuxError::Other(_) => "Other",
123 }
124 }
125
126 pub fn status_code(&self) -> Option<u16> {
133 let message = match self {
134 AiMuxError::Provider(m)
135 | AiMuxError::Http(m)
136 | AiMuxError::Auth(m)
137 | AiMuxError::ApiCall(m)
138 | AiMuxError::ModelNotFound(m) => m,
139 _ => return None,
140 };
141 let rest = message.strip_prefix("HTTP ")?;
143 let code_str: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
144 code_str.parse().ok()
145 }
146}