1use rskit_ai::GenAiError;
4use rskit_errors::{AppError, ErrorCode};
5use serde::Deserialize;
6
7#[derive(Debug, Clone)]
9pub struct ApiError {
10 pub status: u16,
12 pub provider: String,
14 pub message: String,
16 pub error_type: Option<String>,
18}
19
20impl std::fmt::Display for ApiError {
21 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22 write!(
23 f,
24 "{} API error (HTTP {}): {}",
25 self.provider, self.status, self.message
26 )
27 }
28}
29
30impl std::error::Error for ApiError {}
31
32impl ApiError {
33 #[must_use]
35 pub fn to_genai_error(&self) -> GenAiError {
36 let kind = self
37 .error_type
38 .as_deref()
39 .unwrap_or_default()
40 .to_ascii_lowercase();
41 if self.status == 429 || kind.contains("rate") {
42 GenAiError::RateLimited
43 } else if self.status == 404 || kind.contains("model_not_found") {
44 GenAiError::ModelNotFound
45 } else if kind.contains("context") || kind.contains("token") && kind.contains("limit") {
46 GenAiError::ContextLengthExceeded
47 } else if kind.contains("content") || kind.contains("safety") || kind.contains("filter") {
48 GenAiError::ContentFilter
49 } else if self.status == 503 || kind.contains("overloaded") {
50 GenAiError::ModelOverloaded
51 } else {
52 GenAiError::InvalidRequest(self.message.clone())
53 }
54 }
55}
56
57impl From<ApiError> for AppError {
58 fn from(e: ApiError) -> Self {
59 let code = match e.status {
60 401 => ErrorCode::Unauthorized,
61 403 => ErrorCode::Forbidden,
62 404 => ErrorCode::NotFound,
63 429 => ErrorCode::RateLimited,
64 _ => ErrorCode::ExternalService,
65 };
66 Self::new(code, e.to_string())
67 .with_detail("provider", e.provider)
68 .with_detail("status", e.status.to_string())
69 }
70}
71
72#[derive(Deserialize)]
75struct OpenAiErrorBody {
76 error: Option<OpenAiErrorDetail>,
77}
78
79#[derive(Deserialize)]
80struct OpenAiErrorDetail {
81 message: Option<String>,
82 #[serde(rename = "type")]
83 error_type: Option<String>,
84}
85
86pub fn parse_openai_error(status: u16, body: &str) -> ApiError {
88 let (message, error_type) = serde_json::from_str::<OpenAiErrorBody>(body)
89 .ok()
90 .and_then(|b| b.error)
91 .map_or_else(
92 || (body.to_string(), None),
93 |e| (e.message.unwrap_or_else(|| body.to_string()), e.error_type),
94 );
95
96 ApiError {
97 status,
98 provider: "openai".to_string(),
99 message,
100 error_type,
101 }
102}
103
104#[derive(Deserialize)]
107struct AnthropicErrorBody {
108 error: Option<AnthropicErrorDetail>,
109}
110
111#[derive(Deserialize)]
112struct AnthropicErrorDetail {
113 message: Option<String>,
114 #[serde(rename = "type")]
115 error_type: Option<String>,
116}
117
118pub fn parse_anthropic_error(status: u16, body: &str) -> ApiError {
120 let (message, error_type) = serde_json::from_str::<AnthropicErrorBody>(body)
121 .ok()
122 .and_then(|b| b.error)
123 .map_or_else(
124 || (body.to_string(), None),
125 |e| (e.message.unwrap_or_else(|| body.to_string()), e.error_type),
126 );
127
128 ApiError {
129 status,
130 provider: "anthropic".to_string(),
131 message,
132 error_type,
133 }
134}
135
136#[derive(Deserialize)]
139struct GeminiErrorBody {
140 error: Option<GeminiErrorDetail>,
141}
142
143#[derive(Deserialize)]
144struct GeminiErrorDetail {
145 message: Option<String>,
146 status: Option<String>,
147}
148
149pub fn parse_gemini_error(status: u16, body: &str) -> ApiError {
151 let (message, error_type) = serde_json::from_str::<GeminiErrorBody>(body)
152 .ok()
153 .and_then(|b| b.error)
154 .map_or_else(
155 || (body.to_string(), None),
156 |e| (e.message.unwrap_or_else(|| body.to_string()), e.status),
157 );
158
159 ApiError {
160 status,
161 provider: "gemini".to_string(),
162 message,
163 error_type,
164 }
165}
166
167pub const fn estimate_tokens(text: &str) -> usize {
171 text.len() / 4
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn parse_openai_error_structured() {
180 let body = r#"{"error":{"message":"Rate limit exceeded","type":"rate_limit_error"}}"#;
181 let err = parse_openai_error(429, body);
182 assert_eq!(err.status, 429);
183 assert_eq!(err.provider, "openai");
184 assert_eq!(err.message, "Rate limit exceeded");
185 assert_eq!(err.error_type.as_deref(), Some("rate_limit_error"));
186 }
187
188 #[test]
189 fn parse_openai_error_plain_text() {
190 let err = parse_openai_error(500, "internal error");
191 assert_eq!(err.message, "internal error");
192 assert!(err.error_type.is_none());
193 }
194
195 #[test]
196 fn parse_anthropic_error_structured() {
197 let body = r#"{"error":{"message":"Invalid API key","type":"authentication_error"}}"#;
198 let err = parse_anthropic_error(401, body);
199 assert_eq!(err.status, 401);
200 assert_eq!(err.provider, "anthropic");
201 assert_eq!(err.message, "Invalid API key");
202 }
203
204 #[test]
205 fn parse_anthropic_error_plain_text() {
206 let err = parse_anthropic_error(500, "server error");
207 assert_eq!(err.message, "server error");
208 }
209
210 #[test]
211 fn parse_gemini_error_structured() {
212 let body =
213 r#"{"error":{"message":"API key not valid","code":400,"status":"INVALID_ARGUMENT"}}"#;
214 let err = parse_gemini_error(400, body);
215 assert_eq!(err.status, 400);
216 assert_eq!(err.provider, "gemini");
217 assert_eq!(err.message, "API key not valid");
218 assert_eq!(err.error_type.as_deref(), Some("INVALID_ARGUMENT"));
219 }
220
221 #[test]
222 fn parse_gemini_error_plain_text() {
223 let err = parse_gemini_error(500, "oops");
224 assert_eq!(err.message, "oops");
225 }
226
227 #[test]
228 fn api_error_into_app_error() {
229 let api_err = ApiError {
230 status: 429,
231 provider: "openai".to_string(),
232 message: "rate limited".to_string(),
233 error_type: None,
234 };
235 assert_eq!(api_err.to_genai_error(), GenAiError::RateLimited);
236 let converted: AppError = api_err.into();
237 assert_eq!(converted.code(), ErrorCode::RateLimited);
238 }
239
240 #[test]
241 fn estimate_tokens_basic() {
242 assert_eq!(estimate_tokens("hello world!"), 3); assert_eq!(estimate_tokens(""), 0);
244 }
245}