aiclient_api/util/
error.rs1use axum::http::StatusCode;
2use axum::response::{IntoResponse, Response};
3use serde_json::json;
4
5use crate::providers::OutputFormat;
6
7#[derive(Debug, thiserror::Error)]
8pub enum AppError {
9 #[error("Provider error: {0}")]
10 Provider(#[from] anyhow::Error),
11
12 #[error("Authentication required: {0}")]
13 Unauthorized(String),
14
15 #[error("Provider unavailable: {0}")]
16 Unavailable(String),
17
18 #[error("Bad request: {0}")]
19 BadRequest(String),
20
21 #[error("Rate limited")]
22 RateLimited,
23
24 #[error("Upstream error: {status} {body}")]
25 Upstream { status: u16, body: String },
26}
27
28impl AppError {
29 pub fn status_and_message(&self) -> (StatusCode, String) {
31 match self {
32 AppError::Unauthorized(msg) => (StatusCode::UNAUTHORIZED, msg.clone()),
33 AppError::Unavailable(msg) => (StatusCode::SERVICE_UNAVAILABLE, msg.clone()),
34 AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg.clone()),
35 AppError::RateLimited => (StatusCode::TOO_MANY_REQUESTS, "Rate limit exceeded".into()),
36 AppError::Upstream { status, body } => {
37 let code = StatusCode::from_u16(*status).unwrap_or(StatusCode::BAD_GATEWAY);
38 (code, body.clone())
39 }
40 AppError::Provider(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
41 }
42 }
43
44 pub fn openai_error(status: StatusCode, message: &str) -> Response {
46 let error_type = match status {
47 StatusCode::UNAUTHORIZED => "authentication_error",
48 StatusCode::BAD_REQUEST => "invalid_request_error",
49 StatusCode::TOO_MANY_REQUESTS => "rate_limit_error",
50 StatusCode::NOT_FOUND => "not_found_error",
51 _ => "server_error",
52 };
53 let body = json!({
54 "error": {
55 "message": message,
56 "type": error_type,
57 "code": serde_json::Value::Null,
58 }
59 });
60 (status, axum::Json(body)).into_response()
61 }
62
63 pub fn anthropic_error(status: StatusCode, message: &str) -> Response {
65 let error_type = match status {
66 StatusCode::UNAUTHORIZED => "authentication_error",
67 StatusCode::BAD_REQUEST => "invalid_request_error",
68 StatusCode::TOO_MANY_REQUESTS => "rate_limit_error",
69 StatusCode::NOT_FOUND => "not_found_error",
70 StatusCode::SERVICE_UNAVAILABLE => "api_error",
71 StatusCode::FORBIDDEN => "permission_error",
72 _ => "api_error",
73 };
74 let body = json!({
75 "type": "error",
76 "error": {
77 "type": error_type,
78 "message": message,
79 }
80 });
81 (status, axum::Json(body)).into_response()
82 }
83
84 pub fn format_error(status: StatusCode, message: &str, format: OutputFormat) -> Response {
86 match format {
87 OutputFormat::OpenAI => Self::openai_error(status, message),
88 OutputFormat::Anthropic => Self::anthropic_error(status, message),
89 }
90 }
91}
92
93impl IntoResponse for AppError {
94 fn into_response(self) -> Response {
95 let (status, message) = match &self {
96 AppError::Unauthorized(msg) => (StatusCode::UNAUTHORIZED, msg.clone()),
97 AppError::Unavailable(msg) => (StatusCode::SERVICE_UNAVAILABLE, msg.clone()),
98 AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg.clone()),
99 AppError::RateLimited => (StatusCode::TOO_MANY_REQUESTS, "Rate limit exceeded".into()),
100 AppError::Upstream { status, body } => {
101 let code = StatusCode::from_u16(*status).unwrap_or(StatusCode::BAD_GATEWAY);
102 return (code, body.clone()).into_response();
103 }
104 AppError::Provider(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
105 };
106 Self::openai_error(status, &message)
108 }
109}