1use axum::{
2 Json, Router,
3 extract::Request,
4 middleware,
5 response::Response,
6 routing::{get, post},
7};
8use crabllm_core::{Prefix, Provider, Storage};
9
10pub use auth::KeyName;
11pub use state::{AppState, UsageEvent};
12
13pub const PREFIX_KEYS: Prefix = *b"keys";
16pub const PREFIX_RATE_LIMIT: Prefix = *b"rlim";
17pub const PREFIX_USAGE: Prefix = *b"usge";
18pub const PREFIX_CACHE: Prefix = *b"cach";
19pub const PREFIX_BUDGET: Prefix = *b"bdgt";
20pub const PREFIX_AUDIT: Prefix = *b"alog";
21pub const PREFIX_PROVIDERS: Prefix = *b"prvd";
22
23pub mod admin;
24pub mod admin_providers;
25pub mod anthropic;
26pub mod auth;
27pub mod ext;
28pub(crate) mod handlers;
29#[cfg(feature = "openapi")]
30pub mod openapi;
31mod state;
32pub mod storage;
33
34async fn track_active_connections(request: Request, next: middleware::Next) -> Response {
38 metrics::gauge!("crabllm_active_connections").increment(1.0);
39 let response = next.run(request).await;
40 metrics::gauge!("crabllm_active_connections").decrement(1.0);
41 response
42}
43
44pub async fn log_request(request: Request, next: middleware::Next) -> Response {
49 let method = request.method().clone();
50 let path = request.uri().path().to_string();
51 let start = std::time::Instant::now();
52
53 let response = next.run(request).await;
54 let status = response.status();
55 let latency_ms = start.elapsed().as_millis() as u64;
56 let is_probe = path == "/health" || path == "/metrics";
57
58 if is_probe {
59 tracing::debug!(%method, path, status = status.as_u16(), latency_ms, "request");
60 } else if status.is_client_error() || status.is_server_error() {
61 tracing::warn!(%method, path, status = status.as_u16(), latency_ms, "request");
62 } else {
63 tracing::info!(%method, path, status = status.as_u16(), latency_ms, "request");
64 }
65
66 response
67}
68
69pub fn router<S, P>(state: AppState<S, P>, admin_routes: Vec<Router>) -> Router
71where
72 S: Storage + 'static,
73 P: Provider + 'static,
74{
75 let mut app = Router::<AppState<S, P>>::new()
76 .route(
77 "/v1/chat/completions",
78 post(handlers::chat_completions::<S, P>),
79 )
80 .route("/v1/messages", post(anthropic::messages::<S, P>))
81 .route("/v1/embeddings", post(handlers::embeddings::<S, P>))
82 .route(
83 "/v1/images/generations",
84 post(handlers::image_generations::<S, P>),
85 )
86 .route("/v1/audio/speech", post(handlers::audio_speech::<S, P>))
87 .route(
88 "/v1/audio/transcriptions",
89 post(handlers::audio_transcriptions::<S, P>),
90 )
91 .route("/v1/models", get(handlers::models::<S, P>))
92 .route("/v1/usage", get(handlers::usage::<S, P>))
93 .layer(middleware::from_fn_with_state(
94 state.clone(),
95 auth::auth::<S, P>,
96 ))
97 .layer(middleware::from_fn(track_active_connections))
98 .with_state(state);
99
100 app = app.route(
102 "/health",
103 get(|| async { Json(serde_json::json!({"status": "ok"})) }),
104 );
105
106 for admin_router in admin_routes {
109 app = app.merge(admin_router);
110 }
111
112 app
113}