1use std::net::SocketAddr;
2use std::sync::{Arc, Mutex, RwLock};
3use std::time::Instant;
4
5use axum::Json;
6use axum::Router;
7use axum::body::{Body, Bytes, to_bytes};
8use axum::extract::State;
9use axum::http::StatusCode;
10use axum::response::IntoResponse;
11use axum::routing::{get, post};
12use futures_util::StreamExt;
13use tower_http::trace::TraceLayer;
14use tracing::info;
15
16use crate::models::clean_host;
17
18use reqwest::Client;
19
20use crate::backend::web_context;
21
22const HOP_BY_HOP: &[&str] = &[
24 "connection",
25 "keep-alive",
26 "proxy-authenticate",
27 "proxy-authorization",
28 "te",
29 "trailer",
30 "transfer-encoding",
31 "upgrade",
32 "host",
33 "content-length",
34];
35
36pub struct StatusCache {
37 pub models: usize,
38 pub cached_at: Instant,
39}
40
41#[derive(Clone)]
42pub struct ApiState {
43 pub server_url: String,
44 pub api_key: Option<String>,
45 pub model_name: String,
46 pub pid: u32,
47 pub start_time: Instant,
48 pub port: u16,
49 pub client: reqwest::Client,
50 pub status_cache: Arc<RwLock<StatusCache>>,
51 pub system_prompt_preset_name: String,
52 pub web_search_engine: String,
53 pub web_search_engine_url: String,
54 pub web_search_enabled: bool,
55 pub web_search_api_key: Option<String>,
56 pub log_callback: Arc<Mutex<Option<Box<dyn Fn(String) + Send + Sync>>>>,
57}
58
59fn extract_api_key(headers: &axum::http::HeaderMap) -> Option<String> {
60 headers
61 .get("Authorization")
62 .and_then(|v| v.to_str().ok())
63 .and_then(|v| v.strip_prefix("Bearer "))
64 .map(|s| s.to_string())
65}
66
67async fn auth_middleware(
68 State(state): State<ApiState>,
69 req: axum::extract::Request,
70 next: axum::middleware::Next,
71) -> axum::response::Response {
72 tracing::debug!("auth_middleware: api_key={:?}", state.api_key.is_some());
73 if let Some(expected) = &state.api_key {
74 let provided = extract_api_key(req.headers());
75 let expected_bytes = expected.as_bytes();
76 let not_equal = if let Some(provided_str) = provided {
77 constant_time_not_eq(provided_str.as_bytes(), expected_bytes)
78 } else {
79 true
80 };
81 if not_equal {
82 tracing::debug!(
83 "auth_middleware: rejecting request, not_equal={}",
84 not_equal
85 );
86 return (
87 StatusCode::UNAUTHORIZED,
88 Json(serde_json::json!({"error": "Unauthorized"})),
89 )
90 .into_response();
91 }
92 }
93 next.run(req).await
94}
95
96fn constant_time_not_eq(a: &[u8], b: &[u8]) -> bool {
99 if a.len() != b.len() {
100 return true;
101 }
102 let mut result: u8 = 0;
103 for (x, y) in a.iter().zip(b.iter()) {
104 result |= x ^ y;
105 }
106 result != 0
107}
108
109async fn proxy_streaming(
112 State(state): State<ApiState>,
113 req: axum::extract::Request,
114) -> impl IntoResponse {
115 let path = req.uri().path().to_string();
116 let method = req.method().clone();
117 let headers = req.headers().clone();
118
119 let url = format!("{}{}", state.server_url, path);
120
121 if (path == "/v1/chat/completions" || path == "/v1/completions")
123 && method == axum::http::Method::POST
124 {
125 info!("API: proxying {} {}", method, path);
126 {
127 let cb = state.log_callback.lock().unwrap();
128 if let Some(c) = cb.as_ref() {
129 c(format!("API: proxying {} {}", method, path));
130 }
131 }
132 let body_bytes = match to_bytes(req.into_body(), 10 * 1024 * 1024).await {
133 Ok(b) => b,
134 Err(e) => {
135 info!("Failed to collect request body for {}: {}", path, e);
136 return (
137 StatusCode::BAD_GATEWAY,
138 Json(
139 serde_json::json!({"error": format!("Failed to read request body: {}", e)}),
140 ),
141 )
142 .into_response();
143 }
144 };
145
146 let body_bytes = body_bytes;
147 let mut request_json: serde_json::Value = match serde_json::from_slice(&body_bytes) {
148 Ok(j) => j,
149 Err(e) => {
150 info!("Failed to parse request JSON for {}: {}", path, e);
151 return (
152 StatusCode::BAD_REQUEST,
153 Json(serde_json::json!({"error": format!("Invalid JSON: {}", e)})),
154 )
155 .into_response();
156 }
157 };
158
159 info!(
160 "API: web_search_enabled={}, preset='{}', engine='{}'",
161 state.web_search_enabled, state.system_prompt_preset_name, state.web_search_engine
162 );
163 {
164 let cb = state.log_callback.lock().unwrap();
165 if let Some(c) = cb.as_ref() {
166 c(format!(
167 "API: web_search_enabled={}, preset='{}', engine='{}'",
168 state.web_search_enabled,
169 state.system_prompt_preset_name,
170 state.web_search_engine
171 ));
172 }
173 }
174
175 let result = web_context::build_injected_prompt(
176 &state.system_prompt_preset_name,
177 &request_json,
178 state.web_search_enabled,
179 &state.web_search_engine,
180 &state.web_search_engine_url,
181 state.web_search_api_key.as_deref().unwrap_or(""),
182 &state.log_callback,
183 )
184 .await;
185
186 info!(
187 "API: web search performed={}, content_len={}",
188 result.performed,
189 result.content.len()
190 );
191 {
192 let cb = state.log_callback.lock().unwrap();
193 if let Some(c) = cb.as_ref() {
194 c(format!(
195 "API: web search performed={}, content_len={}",
196 result.performed,
197 result.content.len()
198 ));
199 }
200 }
201 if result.performed
202 && !result.content.is_empty()
203 && let Some(obj) = request_json.as_object_mut()
204 && let Some(messages) = obj.get_mut("messages").and_then(|m| m.as_array_mut())
205 && let Some(last) = messages.last_mut()
206 && let Some(content_val) = last.get_mut("content")
207 {
208 *content_val = serde_json::Value::String(result.content);
209 }
210
211 let modified_body = request_json.clone();
212
213 if let Some(messages) = modified_body.get("messages").and_then(|m| m.as_array()) {
214 let last_content = messages
215 .last()
216 .and_then(|m| m.get("content").and_then(|c| c.as_str()))
217 .unwrap_or("");
218 info!(
219 "Prompt to llama-server: {} messages, last content ({} chars):\n{}",
220 messages.len(),
221 last_content.len(),
222 last_content
223 );
224 }
225
226 let body_stream = futures_util::stream::once(async move {
227 Ok::<Bytes, std::convert::Infallible>(Bytes::from(
228 serde_json::to_vec(&modified_body).unwrap_or(body_bytes.to_vec()),
229 ))
230 });
231
232 let mut request_builder = state.client.post(&url);
233
234 let mut filtered = axum::http::HeaderMap::new();
235 for (name, value) in headers.iter() {
236 let n = name.as_str();
237 if !HOP_BY_HOP.contains(&n) && n != "authorization" {
238 filtered.insert(name, value.clone());
239 }
240 }
241 request_builder = request_builder.headers(filtered);
242
243 let response = request_builder
244 .body(reqwest::Body::wrap_stream(body_stream))
245 .send()
246 .await;
247
248 let response = handle_response(response, &path).await;
249 return response.into_response();
250 }
251
252 let body_stream = req
254 .into_body()
255 .into_data_stream()
256 .map(|r| r.map_err(|e| std::io::Error::other(format!("{}", e))));
257
258 let mut request_builder = match method {
259 axum::http::Method::GET => state.client.get(&url),
260 axum::http::Method::POST => state.client.post(&url),
261 axum::http::Method::PUT => state.client.put(&url),
262 axum::http::Method::DELETE => state.client.delete(&url),
263 _ => {
264 return (
265 StatusCode::METHOD_NOT_ALLOWED,
266 Json(serde_json::json!({"error": "Method not supported"})),
267 )
268 .into_response();
269 }
270 };
271
272 let mut filtered = axum::http::HeaderMap::new();
273 for (name, value) in headers.iter() {
274 let n = name.as_str();
275 if !HOP_BY_HOP.contains(&n) && n != "authorization" {
276 filtered.insert(name, value.clone());
277 }
278 }
279 request_builder = request_builder.headers(filtered);
280
281 let response = request_builder
282 .body(reqwest::Body::wrap_stream(body_stream))
283 .send()
284 .await;
285
286 let response = handle_response(response, &path).await;
287 response.into_response()
288}
289
290async fn handle_response(
291 response: Result<reqwest::Response, reqwest::Error>,
292 path: &str,
293) -> impl IntoResponse {
294 match response {
295 Ok(resp) => {
296 let status = resp.status();
297 let headers = resp.headers().clone();
298 let is_sse = resp
299 .headers()
300 .get(axum::http::header::CONTENT_TYPE)
301 .and_then(|v| v.to_str().ok())
302 .map(|v| v.contains("text/event-stream"))
303 .unwrap_or(false);
304
305 if is_sse {
306 let mut response = axum::response::Response::new(Body::from_stream(
307 resp.bytes_stream()
308 .map(|result| result.map_err(std::io::Error::other)),
309 ));
310 *response.status_mut() = status;
311 for (name, value) in headers.iter() {
312 response.headers_mut().insert(name, value.clone());
313 }
314 response
315 } else {
316 let bytes = match resp.bytes().await {
317 Ok(b) => b,
318 Err(e) => {
319 info!("Failed to read response body for {}: {}", path, e);
320 return (
321 StatusCode::BAD_GATEWAY,
322 Json(serde_json::json!({"error": format!("Failed to read backend response: {}", e)})),
323 )
324 .into_response();
325 }
326 };
327 (status, headers, bytes).into_response()
328 }
329 }
330 Err(e) => {
331 info!("Proxy error for {}: {}", path, e);
332 (
333 StatusCode::BAD_GATEWAY,
334 Json(serde_json::json!({"error": format!("Backend unavailable: {}", e)})),
335 )
336 .into_response()
337 }
338 }
339}
340
341async fn security_headers(
343 req: axum::extract::Request,
344 next: axum::middleware::Next,
345) -> impl IntoResponse {
346 let mut resp = next.run(req).await;
347 resp.headers_mut()
348 .entry(axum::http::header::X_CONTENT_TYPE_OPTIONS)
349 .or_insert("nosniff".parse().unwrap());
350 resp.headers_mut()
351 .entry(axum::http::header::X_FRAME_OPTIONS)
352 .or_insert("DENY".parse().unwrap());
353 resp.headers_mut()
354 .entry(axum::http::header::CONTENT_SECURITY_POLICY)
355 .or_insert("default-src 'self'".parse().unwrap());
356 resp
357}
358
359async fn cors_middleware(
362 State(allowed_origins): State<Arc<Vec<String>>>,
363 req: axum::extract::Request,
364 next: axum::middleware::Next,
365) -> impl IntoResponse {
366 let origin = req
367 .headers()
368 .get(axum::http::header::ORIGIN)
369 .and_then(|v| v.to_str().ok())
370 .map(|s| s.to_string());
371
372 let allowed = origin
373 .as_ref()
374 .map(|o| {
375 let origin_host = o
376 .strip_prefix("http://")
377 .or_else(|| o.strip_prefix("https://"))
378 .and_then(|u| u.split('/').next())
379 .map(|h| h.strip_suffix(':').unwrap_or(h))
380 .unwrap_or("");
381 allowed_origins.iter().any(|a| a == origin_host)
382 })
383 .unwrap_or(false);
384
385 if req.method() == axum::http::Method::OPTIONS {
386 if allowed {
387 let mut resp = axum::response::Response::new(Body::empty());
388 resp.headers_mut().insert(
389 axum::http::header::ACCESS_CONTROL_ALLOW_ORIGIN,
390 origin.unwrap().parse().unwrap(),
391 );
392 resp.headers_mut().insert(
393 axum::http::header::ACCESS_CONTROL_ALLOW_METHODS,
394 "GET, POST, PUT, DELETE, OPTIONS".parse().unwrap(),
395 );
396 resp.headers_mut().insert(
397 axum::http::header::ACCESS_CONTROL_ALLOW_HEADERS,
398 "Content-Type, Authorization".parse().unwrap(),
399 );
400 resp
401 } else {
402 StatusCode::METHOD_NOT_ALLOWED.into_response()
403 }
404 } else {
405 let mut resp = next.run(req).await;
406 if allowed {
407 if let Some(o) = origin {
408 resp.headers_mut().insert(
409 axum::http::header::ACCESS_CONTROL_ALLOW_ORIGIN,
410 o.parse().unwrap(),
411 );
412 resp.headers_mut().insert(
413 axum::http::header::ACCESS_CONTROL_ALLOW_HEADERS,
414 "Content-Type, Authorization".parse().unwrap(),
415 );
416 }
417 }
418 resp
419 }
420}
421
422async fn health(State(state): State<ApiState>) -> impl IntoResponse {
424 let resp = state
425 .client
426 .get(format!("{}/health", state.server_url))
427 .send()
428 .await;
429
430 match resp {
431 Ok(response) if response.status().is_success() => Json(serde_json::json!({
432 "status": "ok",
433 "backend": "healthy"
434 })),
435 Ok(_) => Json(serde_json::json!({
436 "status": "degraded",
437 "backend": "unreachable"
438 })),
439 Err(_) => Json(serde_json::json!({
440 "status": "degraded",
441 "backend": "unreachable"
442 })),
443 }
444}
445
446const STATUS_CACHE_TTL: std::time::Duration = std::time::Duration::from_secs(5);
448
449#[axum::debug_handler]
450async fn status(State(state): State<ApiState>) -> impl IntoResponse {
451 let uptime = state.start_time.elapsed();
452 let uptime_secs = uptime.as_secs();
453
454 let loaded_models = {
455 let (is_stale, cached_models) = {
456 let cache = state.status_cache.read().unwrap();
457 (cache.cached_at.elapsed() >= STATUS_CACHE_TTL, cache.models)
458 };
459 if !is_stale {
460 cached_models
461 } else {
462 match state
463 .client
464 .get(format!("{}/models", state.server_url))
465 .send()
466 .await
467 {
468 Ok(resp) if resp.status().is_success() => {
469 let val: Option<serde_json::Value> = resp.json().await.ok();
470 let data = val
471 .as_ref()
472 .and_then(|v| v.get("data"))
473 .and_then(|d| d.as_array());
474 let c = data.map(|a| a.len()).unwrap_or(0);
475 let mut cache = state.status_cache.write().unwrap();
476 cache.models = c;
477 cache.cached_at = Instant::now();
478 c
479 }
480 _ => {
481 let mut cache = state.status_cache.write().unwrap();
482 cache.models = 0;
483 cache.cached_at = Instant::now();
484 0
485 }
486 }
487 }
488 };
489
490 Json(serde_json::json!({
491 "status": "running",
492 "pid": state.pid,
493 "port": state.port,
494 "model": state.model_name,
495 "uptime_seconds": uptime_secs,
496 "loaded_models": loaded_models,
497 }))
498}
499
500pub async fn start_api_server(
501 addr: SocketAddr,
502 api_key: Option<String>,
503 server_port: u16,
504 model_name: String,
505 pid: u32,
506 mut shutdown_rx: tokio::sync::watch::Receiver<bool>,
507 host: String,
508 tls_config: Option<axum_server::tls_rustls::RustlsConfig>,
509 system_prompt_preset_name: String,
510 web_search_engine: String,
511 web_search_engine_url: String,
512 web_search_enabled: bool,
513 web_search_api_key: Option<String>,
514 log_callback: Arc<Mutex<Option<Box<dyn Fn(String) + Send + Sync>>>>,
515) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
516 let bind = addr;
517 let start_time = Instant::now();
518 let client = Client::builder()
519 .pool_max_idle_per_host(20)
520 .timeout(std::time::Duration::from_secs(60))
521 .build()?;
522 let state = ApiState {
523 server_url: format!("http://{}:{}", clean_host(&host), server_port),
524 api_key,
525 model_name,
526 pid,
527 start_time,
528 port: bind.port(),
529 client,
530 status_cache: Arc::new(RwLock::new(StatusCache {
531 models: 0,
532 cached_at: Instant::now() - std::time::Duration::from_secs(10),
533 })),
534 system_prompt_preset_name,
535 web_search_engine,
536 web_search_engine_url,
537 web_search_enabled,
538 web_search_api_key,
539 log_callback,
540 };
541
542 let allowed_origins: Arc<Vec<String>> = Arc::new({
543 let mut origins = vec!["127.0.0.1".into(), "localhost".into()];
544 if host != "127.0.0.1" && host != "localhost" && host != "0.0.0.0" {
545 origins.push(host.clone());
546 }
547 origins
548 });
549
550 let api_key_clone = state.api_key.clone();
551 let protocol = if tls_config.is_some() {
552 "https"
553 } else {
554 "http"
555 };
556 info!(
557 "API server starting on {protocol}://{} (proxying to http://127.0.0.1:{})",
558 host, server_port
559 );
560 if api_key_clone.is_some() {
561 info!("API key authentication is ENABLED");
562 }
563
564 let app = Router::new()
565 .route("/health", get(health))
566 .route("/metrics", get(proxy_streaming))
567 .merge(
568 Router::new()
569 .route("/v1/chat/completions", post(proxy_streaming))
570 .route("/v1/completions", post(proxy_streaming))
571 .route("/v1/embeddings", post(proxy_streaming))
572 .route("/v1/models", get(proxy_streaming))
573 .route("/api/status", get(status))
574 .fallback(proxy_streaming)
575 .layer(axum::middleware::from_fn_with_state(
576 allowed_origins.clone(),
577 cors_middleware,
578 ))
579 .layer(TraceLayer::new_for_http()),
580 )
581 .layer(axum::middleware::from_fn(security_headers))
582 .layer(axum::middleware::from_fn_with_state(
583 state.clone(),
584 auth_middleware,
585 ))
586 .with_state(state);
587
588 match tls_config {
589 Some(tls_cfg) => {
590 let tls_listener = axum_server::bind_rustls(bind, tls_cfg);
591 let shutdown_fut = async {
592 let _ = shutdown_rx.wait_for(|v| *v).await;
593 };
594 tokio::select! {
595 result = tls_listener.serve(app.into_make_service()) => result?,
596 _ = shutdown_fut => {},
597 };
598 }
599 None => {
600 axum::serve(tokio::net::TcpListener::bind(bind).await?, app)
601 .with_graceful_shutdown(async move {
602 let _ = shutdown_rx.wait_for(|v| *v).await;
603 })
604 .await?;
605 }
606 }
607 Ok(())
608}