1use std::net::SocketAddr;
2use std::time::Instant;
3
4use axum::Json;
5use axum::Router;
6use axum::body::Body;
7use axum::extract::State;
8use axum::http::StatusCode;
9use axum::response::IntoResponse;
10use axum::routing::{get, post};
11use futures_util::{StreamExt, stream};
12use tower_http::cors::CorsLayer;
13use tower_http::trace::TraceLayer;
14use tracing::info;
15
16use reqwest::Client;
17
18#[derive(Clone)]
19pub struct ApiState {
20 pub server_url: String,
21 pub api_key: Option<String>,
22 pub model_name: String,
23 pub pid: u32,
24 pub start_time: Instant,
25 pub port: u16,
26 pub client: reqwest::Client,
27}
28
29fn extract_api_key(headers: &axum::http::HeaderMap) -> Option<String> {
30 headers
31 .get("Authorization")
32 .and_then(|v| v.to_str().ok())
33 .and_then(|v| v.strip_prefix("Bearer "))
34 .map(|s| s.to_string())
35}
36
37async fn auth_middleware(
38 State(state): State<ApiState>,
39 req: axum::extract::Request,
40 next: axum::middleware::Next,
41) -> axum::response::Response {
42 if let Some(expected) = &state.api_key {
43 let provided = extract_api_key(req.headers());
44 if provided.as_deref() != Some(expected) {
45 return (
46 StatusCode::UNAUTHORIZED,
47 Json(serde_json::json!({"error": "Unauthorized"})),
48 )
49 .into_response();
50 }
51 }
52 next.run(req).await
53}
54
55async fn proxy_streaming(
58 State(state): State<ApiState>,
59 req: axum::extract::Request,
60) -> impl IntoResponse {
61 let path = req.uri().path().to_string();
62 let method = req.method().clone();
63 let headers = req.headers().clone();
64
65 let url = format!("{}{}", state.server_url, path);
66
67 let body_bytes = match axum::body::to_bytes(req.into_body(), 10 * 1024 * 1024).await {
69 Ok(b) => b,
70 Err(e) => {
71 info!("Failed to read request body for {}: {}", path, e);
72 return (
73 StatusCode::BAD_REQUEST,
74 Json(serde_json::json!({"error": format!("Failed to read request body: {}", e)})),
75 )
76 .into_response();
77 }
78 };
79 let body_stream = stream::iter(vec![Ok::<_, reqwest::Error>(body_bytes)]);
80
81 let mut request_builder = match method {
82 axum::http::Method::GET => state.client.get(&url),
83 axum::http::Method::POST => state.client.post(&url),
84 axum::http::Method::PUT => state.client.put(&url),
85 axum::http::Method::DELETE => state.client.delete(&url),
86 _ => {
87 return (
88 StatusCode::METHOD_NOT_ALLOWED,
89 Json(serde_json::json!({"error": "Method not supported"})),
90 )
91 .into_response();
92 }
93 };
94
95 const HOP_BY_HOP: &[&str] = &[
96 "connection",
97 "keep-alive",
98 "proxy-authenticate",
99 "proxy-authorization",
100 "te",
101 "trailer",
102 "transfer-encoding",
103 "upgrade",
104 "host",
105 ];
106 for (name, value) in headers.iter() {
107 let name_str = name.as_str();
108 if !HOP_BY_HOP.contains(&name_str) && name_str != "authorization" {
109 request_builder = request_builder.header(name, value);
110 }
111 }
112
113 let response = request_builder
114 .body(reqwest::Body::wrap_stream(body_stream))
115 .send()
116 .await;
117
118 match response {
119 Ok(resp) => {
120 let status = resp.status();
121 let headers = resp.headers().clone();
122 let is_sse = resp
123 .headers()
124 .get(axum::http::header::CONTENT_TYPE)
125 .and_then(|v| v.to_str().ok())
126 .map(|v| v.contains("text/event-stream"))
127 .unwrap_or(false);
128
129 if is_sse {
130 let mut response = axum::response::Response::new(Body::from_stream(
131 resp.bytes_stream()
132 .map(|result| result.map_err(std::io::Error::other)),
133 ));
134 *response.status_mut() = status;
135 for (name, value) in headers.iter() {
136 response.headers_mut().insert(name, value.clone());
137 }
138 response
139 } else {
140 let bytes = match resp.bytes().await {
141 Ok(b) => b,
142 Err(e) => {
143 info!("Failed to read response body for {}: {}", path, e);
144 return (
145 StatusCode::BAD_GATEWAY,
146 Json(serde_json::json!({"error": format!("Failed to read backend response: {}", e)})),
147 )
148 .into_response();
149 }
150 };
151 (status, headers, bytes).into_response()
152 }
153 }
154 Err(e) => {
155 info!("Proxy error for {}: {}", path, e);
156 (
157 StatusCode::BAD_GATEWAY,
158 Json(serde_json::json!({"error": format!("Backend unavailable: {}", e)})),
159 )
160 .into_response()
161 }
162 }
163}
164
165async fn health(State(state): State<ApiState>) -> impl IntoResponse {
167 let resp = state
168 .client
169 .get(format!("{}/health", state.server_url))
170 .send()
171 .await;
172
173 match resp {
174 Ok(response) if response.status().is_success() => Json(serde_json::json!({
175 "status": "ok",
176 "backend": "healthy"
177 })),
178 Ok(_) => Json(serde_json::json!({
179 "status": "degraded",
180 "backend": "unreachable"
181 })),
182 Err(_) => Json(serde_json::json!({
183 "status": "degraded",
184 "backend": "unreachable"
185 })),
186 }
187}
188
189async fn status(State(state): State<ApiState>) -> impl IntoResponse {
191 let uptime = state.start_time.elapsed();
192 let uptime_secs = uptime.as_secs();
193
194 let loaded_models = match state
196 .client
197 .get(format!("{}/models", state.server_url))
198 .send()
199 .await
200 {
201 Ok(resp) if resp.status().is_success() => {
202 let json: serde_json::Value = match resp.json().await {
203 Ok(v) => v,
204 Err(_) => serde_json::json!([]),
205 };
206 json.get("data")
207 .and_then(|d| d.as_array())
208 .map(|a| a.len())
209 .unwrap_or(0)
210 }
211 _ => 0,
212 };
213
214 Json(serde_json::json!({
215 "status": "running",
216 "pid": state.pid,
217 "port": state.port,
218 "model": state.model_name,
219 "uptime_seconds": uptime_secs,
220 "loaded_models": loaded_models,
221 }))
222}
223
224pub async fn start_api_server(
225 addr: SocketAddr,
226 api_key: Option<String>,
227 server_port: u16,
228 model_name: String,
229 pid: u32,
230 mut shutdown_rx: tokio::sync::watch::Receiver<bool>,
231 host: String,
232 tls_config: Option<axum_server::tls_rustls::RustlsConfig>,
233) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
234 let bind = addr;
235 let start_time = Instant::now();
236 let client = Client::builder()
237 .pool_max_idle_per_host(20)
238 .timeout(std::time::Duration::from_secs(300))
239 .build()?;
240 let state = ApiState {
241 server_url: format!("http://127.0.0.1:{}", server_port),
242 api_key,
243 model_name,
244 pid,
245 start_time,
246 port: bind.port(),
247 client,
248 };
249
250 let cors = CorsLayer::new()
251 .allow_origin(tower_http::cors::Any)
252 .allow_methods([
253 axum::http::Method::GET,
254 axum::http::Method::POST,
255 axum::http::Method::PUT,
256 axum::http::Method::DELETE,
257 axum::http::Method::OPTIONS,
258 ])
259 .allow_headers([
260 axum::http::header::CONTENT_TYPE,
261 axum::http::header::AUTHORIZATION,
262 ]);
263
264 let api_key_clone = state.api_key.clone();
265 let protocol = if tls_config.is_some() {
266 "https"
267 } else {
268 "http"
269 };
270 info!(
271 "API server starting on {protocol}://{} (proxying to http://127.0.0.1:{})",
272 host, server_port
273 );
274 if api_key_clone.is_some() {
275 info!("API key authentication is ENABLED");
276 }
277
278 let app = Router::new()
279 .route("/health", get(health))
280 .route("/metrics", get(proxy_streaming))
281 .merge(
282 Router::new()
283 .route("/v1/chat/completions", post(proxy_streaming))
284 .route("/v1/completions", post(proxy_streaming))
285 .route("/v1/embeddings", post(proxy_streaming))
286 .route("/v1/models", get(proxy_streaming))
287 .route("/api/status", get(status))
288 .fallback(proxy_streaming)
289 .layer(cors)
290 .layer(TraceLayer::new_for_http())
291 .layer(axum::middleware::from_fn_with_state(
292 state.clone(),
293 auth_middleware,
294 )),
295 )
296 .with_state(state);
297
298 match tls_config {
299 Some(tls_cfg) => {
300 let tls_listener = axum_server::bind_rustls(bind, tls_cfg);
301 let shutdown_fut = async {
302 let _ = shutdown_rx.wait_for(|v| *v).await;
303 };
304 tokio::select! {
305 result = tls_listener.serve(app.into_make_service()) => result?,
306 _ = shutdown_fut => {},
307 };
308 }
309 None => {
310 axum::serve(tokio::net::TcpListener::bind(bind).await?, app)
311 .with_graceful_shutdown(async move {
312 let _ = shutdown_rx.wait_for(|v| *v).await;
313 })
314 .await?;
315 }
316 }
317 Ok(())
318}