feat: add Axum route handlers for /v1/chat/completions, /health, /v1/models

This commit is contained in:
2026-06-05 23:37:38 +07:00
parent 3916ef3158
commit e19fcd5417
+120
View File
@@ -0,0 +1,120 @@
use axum::{
body::Body,
extract::State,
http::{HeaderMap, HeaderValue, StatusCode},
response::{IntoResponse, Response},
Json,
};
use futures::StreamExt;
use serde_json::{json, Value};
use tracing::{error, info};
use crate::proxy::{ProxyClient, ProxyResponse};
use crate::types::ChatCompletionRequest;
#[derive(Clone)]
pub struct AppState {
pub proxy: ProxyClient,
}
pub async fn health() -> impl IntoResponse {
Json(json!({"status": "ok"}))
}
pub async fn chat_completions(
State(state): State<AppState>,
Json(req): Json<ChatCompletionRequest>,
) -> Response {
let is_streaming = req.stream.unwrap_or(false);
info!(model = %req.model, streaming = is_streaming, "chat completion request");
match state.proxy.forward_request(req).await {
Ok(ProxyResponse::Buffered(bytes)) => {
let mut headers = HeaderMap::new();
headers.insert(
axum::http::header::CONTENT_TYPE,
HeaderValue::from_static("application/json"),
);
(StatusCode::OK, headers, bytes).into_response()
}
Ok(ProxyResponse::Streamed(upstream_resp)) => {
let status = upstream_resp.status();
let upstream_headers = upstream_resp.headers().clone();
let stream = upstream_resp.bytes_stream().map(|result| {
result.map_err(|e| {
error!("stream error: {}", e);
std::io::Error::new(std::io::ErrorKind::Other, e)
})
});
let body = Body::from_stream(stream);
let mut resp = Response::new(body);
*resp.status_mut() = status;
if let Some(ct) = upstream_headers.get(axum::http::header::CONTENT_TYPE) {
resp.headers_mut().insert(axum::http::header::CONTENT_TYPE, ct.clone());
}
resp
}
Err(e) => {
error!("proxy error: {}", e);
let body = Json(json!({
"error": {
"message": e.to_string(),
"type": "proxy_error",
"code": "upstream_error"
}
}));
(StatusCode::BAD_GATEWAY, body).into_response()
}
}
}
pub async fn models(State(state): State<AppState>) -> Response {
match state.proxy.forward_get("/v1/models").await {
Ok(bytes) => {
let mut headers = HeaderMap::new();
headers.insert(
axum::http::header::CONTENT_TYPE,
HeaderValue::from_static("application/json"),
);
(StatusCode::OK, headers, bytes).into_response()
}
Err(e) => {
error!("models proxy error: {}", e);
(
StatusCode::BAD_GATEWAY,
Json(json!({"error": {"message": e.to_string()}})),
)
.into_response()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use axum::{body::to_bytes, http::Request, routing::get};
use tower::util::ServiceExt;
#[tokio::test]
async fn test_health_returns_200() {
let app = axum::Router::new().route("/health", get(health));
let response = app
.oneshot(
Request::builder()
.uri("/health")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = to_bytes(response.into_body(), usize::MAX).await.unwrap();
let json: Value = serde_json::from_slice(&body).unwrap();
assert_eq!(json["status"], "ok");
}
}