1use crate::http::HttpError;
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct HttpErrorInfo {
13 pub status: u16,
15 pub url: String,
17 pub message: String,
19 pub body_snippet: Option<String>,
21}
22
23impl std::fmt::Display for HttpErrorInfo {
24 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 write!(f, "HTTP {} for {}: {}", self.status, self.url, self.message)?;
26 if let Some(ref snippet) = self.body_snippet {
27 let truncated: String = snippet.chars().take(200).collect();
28 write!(f, " | body[0:200]={}", truncated)?;
29 }
30 Ok(())
31 }
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct UsageLimitInfo {
39 pub limit_type: String,
41 pub api: String,
43 pub current: f64,
45 pub limit: f64,
47 pub tier: String,
49 pub retry_after_seconds: Option<i64>,
51 pub upgrade_url: String,
53}
54
55impl Default for UsageLimitInfo {
56 fn default() -> Self {
57 Self {
58 limit_type: String::new(),
59 api: String::new(),
60 current: 0.0,
61 limit: 0.0,
62 tier: "free".to_string(),
63 retry_after_seconds: None,
64 upgrade_url: "https://usesynth.ai/pricing".to_string(),
65 }
66 }
67}
68
69impl std::fmt::Display for UsageLimitInfo {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 write!(
72 f,
73 "Rate limit exceeded: {} ({}/{}) for tier '{}'. Upgrade at {}",
74 self.limit_type, self.current, self.limit, self.tier, self.upgrade_url
75 )
76 }
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct JobErrorInfo {
82 pub job_id: String,
84 pub message: String,
86 pub code: Option<String>,
88}
89
90impl std::fmt::Display for JobErrorInfo {
91 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92 write!(f, "Job {} failed: {}", self.job_id, self.message)?;
93 if let Some(ref code) = self.code {
94 write!(f, " (code: {})", code)?;
95 }
96 Ok(())
97 }
98}
99
100#[derive(Debug, Error)]
105pub enum CoreError {
106 #[error("invalid input: {0}")]
108 InvalidInput(String),
109
110 #[error("url parse error: {0}")]
112 UrlParse(#[from] url::ParseError),
113
114 #[error("http error: {0}")]
116 Http(#[from] reqwest::Error),
117
118 #[error("{0}")]
120 HttpResponse(HttpErrorInfo),
121
122 #[error("authentication failed: {0}")]
124 Authentication(String),
125
126 #[error("validation error: {0}")]
128 Validation(String),
129
130 #[error("{0}")]
132 UsageLimit(UsageLimitInfo),
133
134 #[error("{0}")]
136 Job(JobErrorInfo),
137
138 #[error("config error: {0}")]
140 Config(String),
141
142 #[error("timeout: {0}")]
144 Timeout(String),
145
146 #[error("protocol error: {0}")]
148 Protocol(String),
149
150 #[error("internal error: {0}")]
152 Internal(String),
153}
154
155impl CoreError {
156 pub fn http_response(status: u16, url: &str, message: &str, body: Option<&str>) -> Self {
158 CoreError::HttpResponse(HttpErrorInfo {
159 status,
160 url: url.to_string(),
161 message: message.to_string(),
162 body_snippet: body.map(|s| s.chars().take(200).collect()),
163 })
164 }
165
166 pub fn auth(message: impl Into<String>) -> Self {
168 CoreError::Authentication(message.into())
169 }
170
171 pub fn validation(message: impl Into<String>) -> Self {
173 CoreError::Validation(message.into())
174 }
175
176 pub fn usage_limit(
178 limit_type: &str,
179 api: &str,
180 current: f64,
181 limit: f64,
182 tier: &str,
183 retry_after: Option<i64>,
184 ) -> Self {
185 CoreError::UsageLimit(UsageLimitInfo {
186 limit_type: limit_type.to_string(),
187 api: api.to_string(),
188 current,
189 limit,
190 tier: tier.to_string(),
191 retry_after_seconds: retry_after,
192 upgrade_url: "https://usesynth.ai/pricing".to_string(),
193 })
194 }
195
196 pub fn job(job_id: &str, message: &str, code: Option<&str>) -> Self {
198 CoreError::Job(JobErrorInfo {
199 job_id: job_id.to_string(),
200 message: message.to_string(),
201 code: code.map(String::from),
202 })
203 }
204
205 pub fn timeout(message: impl Into<String>) -> Self {
207 CoreError::Timeout(message.into())
208 }
209
210 pub fn config(message: impl Into<String>) -> Self {
212 CoreError::Config(message.into())
213 }
214
215 pub fn is_auth_error(&self) -> bool {
217 matches!(self, CoreError::Authentication(_))
218 }
219
220 pub fn is_rate_limit(&self) -> bool {
222 matches!(self, CoreError::UsageLimit(_))
223 }
224
225 pub fn is_retryable(&self) -> bool {
227 match self {
228 CoreError::HttpResponse(info) => info.status >= 500,
229 CoreError::Http(_) => true,
230 CoreError::Timeout(_) => true,
231 _ => false,
232 }
233 }
234
235 pub fn http_status(&self) -> Option<u16> {
237 match self {
238 CoreError::HttpResponse(info) => Some(info.status),
239 CoreError::Http(e) => e.status().map(|s| s.as_u16()),
240 _ => None,
241 }
242 }
243}
244
245impl From<HttpError> for CoreError {
246 fn from(err: HttpError) -> Self {
247 match err {
248 HttpError::Request(e) => CoreError::Http(e),
249 HttpError::Response(detail) => CoreError::HttpResponse(HttpErrorInfo {
250 status: detail.status,
251 url: detail.url,
252 message: detail.message,
253 body_snippet: detail.body_snippet,
254 }),
255 HttpError::InvalidUrl(msg) => CoreError::InvalidInput(msg),
256 HttpError::JsonParse(msg) => CoreError::Protocol(msg),
257 }
258 }
259}
260
261pub type CoreResult<T> = Result<T, CoreError>;
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 #[test]
269 fn test_http_error_display() {
270 let err = CoreError::http_response(404, "https://api.example.com/test", "not found", None);
271 let msg = format!("{}", err);
272 assert!(msg.contains("404"));
273 assert!(msg.contains("api.example.com"));
274 }
275
276 #[test]
277 fn test_usage_limit_display() {
278 let err = CoreError::usage_limit(
279 "inference_tokens_per_day",
280 "inference",
281 10000.0,
282 5000.0,
283 "free",
284 Some(3600),
285 );
286 let msg = format!("{}", err);
287 assert!(msg.contains("inference_tokens_per_day"));
288 assert!(msg.contains("free"));
289 }
290
291 #[test]
292 fn test_retryable() {
293 let err_500 =
294 CoreError::http_response(500, "https://api.example.com", "server error", None);
295 assert!(err_500.is_retryable());
296
297 let err_404 = CoreError::http_response(404, "https://api.example.com", "not found", None);
298 assert!(!err_404.is_retryable());
299
300 let err_auth = CoreError::auth("invalid key");
301 assert!(!err_auth.is_retryable());
302 }
303
304 #[test]
305 fn test_http_status() {
306 let err = CoreError::http_response(403, "https://api.example.com", "forbidden", None);
307 assert_eq!(err.http_status(), Some(403));
308
309 let err_auth = CoreError::auth("invalid key");
310 assert_eq!(err_auth.http_status(), None);
311 }
312}