litellm_rs/server/routes/
mod.rs1pub mod admin;
6pub mod admin_dashboard;
7mod admin_openapi;
8mod admin_providers;
9mod admin_request_ledger;
10mod admin_routing;
11mod admin_routing_policy;
12pub mod ai;
13pub mod auth;
14pub mod budget;
15pub mod health;
16pub mod keys;
17#[cfg(feature = "mcp")]
18pub mod mcp;
19pub mod pricing;
20pub mod teams;
21
22use actix_web::HttpResponse;
23
24#[derive(Debug, Clone, serde::Serialize)]
26pub struct ApiResponse<T> {
27 pub success: bool,
29 #[serde(skip_serializing_if = "Option::is_none")]
31 pub data: Option<T>,
32 #[serde(skip_serializing_if = "Option::is_none")]
34 pub error: Option<String>,
35 #[serde(skip_serializing_if = "Option::is_none")]
37 pub meta: Option<serde_json::Value>,
38}
39
40impl<T> ApiResponse<T>
41where
42 T: serde::Serialize,
43{
44 pub fn success(data: T) -> Self {
46 Self {
47 success: true,
48 data: Some(data),
49 error: None,
50 meta: None,
51 }
52 }
53
54 pub fn error(message: String) -> ApiResponse<()> {
56 ApiResponse {
57 success: false,
58 data: None,
59 error: Some(message),
60 meta: None,
61 }
62 }
63}
64
65impl<T> ApiResponse<T> {
66 pub fn error_for_type(message: String) -> ApiResponse<T> {
68 ApiResponse {
69 success: false,
70 data: None,
71 error: Some(message),
72 meta: None,
73 }
74 }
75}
76
77impl<T> ApiResponse<T>
78where
79 T: serde::Serialize,
80{
81 #[deprecated(note = "Use explicit route status helpers instead")]
83 pub fn to_http_response(&self) -> HttpResponse {
84 if self.success {
85 HttpResponse::Ok().json(self)
86 } else {
87 HttpResponse::BadRequest().json(self)
88 }
89 }
90}
91
92#[derive(Debug, Clone, serde::Serialize)]
94pub struct PaginationMeta {
95 pub page: u32,
97 pub limit: u32,
99 pub total: u64,
101 pub pages: u32,
103 pub has_next: bool,
105 pub has_prev: bool,
107}
108
109impl PaginationMeta {
110 pub fn new(page: u32, limit: u32, total: u64) -> Self {
112 let pages = ((total as f64) / (limit as f64)).ceil() as u32;
113
114 Self {
115 page,
116 limit,
117 total,
118 pages,
119 has_next: page < pages,
120 has_prev: page > 1,
121 }
122 }
123}
124
125#[derive(Debug, Clone, serde::Serialize)]
127pub struct PaginatedResponse<T> {
128 pub items: Vec<T>,
130 pub pagination: PaginationMeta,
132}
133
134impl<T> PaginatedResponse<T>
135where
136 T: serde::Serialize,
137{
138 pub fn new(items: Vec<T>, page: u32, limit: u32, total: u64) -> Self {
140 Self {
141 items,
142 pagination: PaginationMeta::new(page, limit, total),
143 }
144 }
145}
146
147#[derive(Debug, Clone, serde::Deserialize)]
149pub struct PaginationQuery {
150 #[serde(default = "default_page")]
152 pub page: u32,
153 #[serde(default = "default_limit")]
155 pub limit: u32,
156}
157
158fn default_page() -> u32 {
159 1
160}
161
162fn default_limit() -> u32 {
163 20
164}
165
166impl PaginationQuery {
167 pub fn validate(&self) -> Result<(), String> {
169 if self.page == 0 {
170 return Err("Page must be greater than 0".to_string());
171 }
172 if self.limit == 0 {
173 return Err("Limit must be greater than 0".to_string());
174 }
175 if self.limit > 1000 {
176 return Err("Limit cannot exceed 1000".to_string());
177 }
178 Ok(())
179 }
180
181 pub fn offset(&self) -> u32 {
183 (self.page - 1) * self.limit
184 }
185}
186
187#[derive(Debug, Clone, serde::Deserialize)]
189pub struct SortQuery {
190 #[serde(default = "default_sort_by")]
192 pub sort_by: String,
193 #[serde(default = "default_sort_order")]
195 pub sort_order: String,
196}
197
198fn default_sort_by() -> String {
199 "created_at".to_string()
200}
201
202fn default_sort_order() -> String {
203 "desc".to_string()
204}
205
206impl SortQuery {
207 pub fn validate(&self, valid_fields: &[&str]) -> Result<(), String> {
209 if !valid_fields.contains(&self.sort_by.as_str()) {
210 return Err(format!("Invalid sort field: {}", self.sort_by));
211 }
212 if !["asc", "desc"].contains(&self.sort_order.as_str()) {
213 return Err("Sort order must be 'asc' or 'desc'".to_string());
214 }
215 Ok(())
216 }
217}
218
219#[derive(Debug, Clone, serde::Deserialize)]
221pub struct ListQuery {
222 #[serde(flatten)]
224 pub pagination: PaginationQuery,
225 #[serde(flatten)]
227 pub sort: SortQuery,
228}
229
230impl ListQuery {
231 pub fn validate(&self, valid_sort_fields: &[&str]) -> Result<(), String> {
233 self.pagination.validate()?;
234 self.sort.validate(valid_sort_fields)?;
235 Ok(())
236 }
237}
238
239pub mod errors {
241 use super::*;
242 use crate::utils::error::gateway_error::GatewayError;
243 use actix_web::ResponseError;
244
245 pub fn gateway_error_to_response(error: GatewayError) -> HttpResponse {
249 error.error_response()
250 }
251
252 pub fn validation_error(message: &str) -> HttpResponse {
254 HttpResponse::BadRequest().json(ApiResponse::<()>::error(message.to_string()))
255 }
256
257 pub fn unauthorized_error(message: &str) -> HttpResponse {
259 HttpResponse::Unauthorized().json(ApiResponse::<()>::error(message.to_string()))
260 }
261
262 pub fn forbidden_error(message: &str) -> HttpResponse {
264 HttpResponse::Forbidden().json(ApiResponse::<()>::error(message.to_string()))
265 }
266
267 pub fn not_found_error(message: &str) -> HttpResponse {
269 HttpResponse::NotFound().json(ApiResponse::<()>::error(message.to_string()))
270 }
271
272 pub fn internal_error(message: &str) -> HttpResponse {
274 HttpResponse::InternalServerError().json(ApiResponse::<()>::error(message.to_string()))
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281
282 #[test]
283 fn test_api_response_success() {
284 let response = ApiResponse::success("test data");
285 assert!(response.success);
286 assert_eq!(response.data, Some("test data"));
287 assert!(response.error.is_none());
288 }
289
290 #[test]
291 fn test_api_response_error() {
292 let response = ApiResponse::<()>::error("test error".to_string());
293 assert!(!response.success);
294 assert!(response.data.is_none());
295 assert_eq!(response.error, Some("test error".to_string()));
296 }
297
298 #[test]
299 #[allow(deprecated)]
300 fn test_api_response_to_http_response_remains_compatibility_shim() {
301 let success = ApiResponse::success("test data").to_http_response();
302 assert_eq!(success.status(), actix_web::http::StatusCode::OK);
303
304 let error = ApiResponse::<()>::error("test error".to_string()).to_http_response();
305 assert_eq!(error.status(), actix_web::http::StatusCode::BAD_REQUEST);
306 }
307
308 #[actix_web::test]
309 async fn test_gateway_error_helpers_keep_api_response_envelope() {
310 let response = errors::validation_error("test validation error");
311
312 assert_eq!(response.status(), actix_web::http::StatusCode::BAD_REQUEST);
313 let body = actix_web::body::to_bytes(response.into_body())
314 .await
315 .unwrap();
316 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
317
318 assert_eq!(json["success"], false);
319 assert_eq!(json["error"], "test validation error");
320 assert!(json["data"].is_null());
321 }
322
323 #[test]
324 fn test_error_helpers_preserve_specific_http_statuses() {
325 assert_eq!(
326 errors::unauthorized_error("unauthorized").status(),
327 actix_web::http::StatusCode::UNAUTHORIZED
328 );
329 assert_eq!(
330 errors::forbidden_error("forbidden").status(),
331 actix_web::http::StatusCode::FORBIDDEN
332 );
333 assert_eq!(
334 errors::not_found_error("missing").status(),
335 actix_web::http::StatusCode::NOT_FOUND
336 );
337 assert_eq!(
338 errors::internal_error("internal").status(),
339 actix_web::http::StatusCode::INTERNAL_SERVER_ERROR
340 );
341 }
342
343 #[test]
344 fn test_pagination_meta() {
345 let meta = PaginationMeta::new(2, 10, 25);
346 assert_eq!(meta.page, 2);
347 assert_eq!(meta.limit, 10);
348 assert_eq!(meta.total, 25);
349 assert_eq!(meta.pages, 3);
350 assert!(meta.has_next);
351 assert!(meta.has_prev);
352 }
353
354 #[test]
355 fn test_pagination_query_validation() {
356 let valid_query = PaginationQuery { page: 1, limit: 20 };
357 assert!(valid_query.validate().is_ok());
358
359 let invalid_page = PaginationQuery { page: 0, limit: 20 };
360 assert!(invalid_page.validate().is_err());
361
362 let invalid_limit = PaginationQuery { page: 1, limit: 0 };
363 assert!(invalid_limit.validate().is_err());
364
365 let too_large_limit = PaginationQuery {
366 page: 1,
367 limit: 2000,
368 };
369 assert!(too_large_limit.validate().is_err());
370 }
371
372 #[test]
373 fn test_pagination_query_offset() {
374 let query = PaginationQuery { page: 3, limit: 10 };
375 assert_eq!(query.offset(), 20);
376 }
377
378 #[test]
379 fn test_sort_query_validation() {
380 let valid_fields = &["name", "created_at", "updated_at"];
381
382 let valid_query = SortQuery {
383 sort_by: "name".to_string(),
384 sort_order: "asc".to_string(),
385 };
386 assert!(valid_query.validate(valid_fields).is_ok());
387
388 let invalid_field = SortQuery {
389 sort_by: "invalid".to_string(),
390 sort_order: "asc".to_string(),
391 };
392 assert!(invalid_field.validate(valid_fields).is_err());
393
394 let invalid_order = SortQuery {
395 sort_by: "name".to_string(),
396 sort_order: "invalid".to_string(),
397 };
398 assert!(invalid_order.validate(valid_fields).is_err());
399 }
400}
401
402#[cfg(feature = "a2a")]
403pub mod a2a;