feat: add Axum route handlers for /v1/chat/completions, /health, /v1/models
This commit is contained in:
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user