pub mod api_types;
pub mod cache_handlers;
pub mod channel;
pub mod dispatcher;
pub mod handlers;
pub mod inference_worker;
pub mod middleware;
pub mod state;
pub mod worker;
#[cfg(feature = "server")]
pub mod backpressure;
#[cfg(feature = "server")]
pub mod metrics;
#[cfg(feature = "server")]
pub mod rate_limiter;
pub use middleware::{
API_KEY_HEADER, ApiKeyConfig, MAX_REQUEST_SIZE, REQUEST_ID_HEADER, authenticate_api_key,
extract_request_id, inject_request_id, limit_request_size,
};
pub use state::{AppState, ServerConfig, ServerConfigBuilder};
pub use crate::{EngineConfig, ModelConfig, NormalizationMode, PoolingStrategy};
use axum::{
Router,
extract::State,
http::StatusCode,
response::{IntoResponse, Json},
routing::get,
};
use serde_json::json;
use std::net::SocketAddr;
use std::path::PathBuf;
use tokio::signal;
use tower_http::cors::CorsLayer;
use tower_http::trace::TraceLayer;
use tracing::{info, warn};
use uuid::Uuid;
#[async_trait::async_trait]
pub trait ModelProvider: Send + Sync {
async fn get_model_path(&self, model_name: &str) -> crate::Result<PathBuf>;
async fn list_models(&self) -> crate::Result<Vec<crate::ModelInfo>>;
}
pub struct FileModelProvider {
model_path: PathBuf,
model_name: String,
}
impl FileModelProvider {
pub fn new(model_path: impl Into<PathBuf>, model_name: impl Into<String>) -> Self {
Self {
model_path: model_path.into(),
model_name: model_name.into(),
}
}
}
#[async_trait::async_trait]
impl ModelProvider for FileModelProvider {
async fn get_model_path(&self, model_name: &str) -> crate::Result<PathBuf> {
if model_name == self.model_name {
Ok(self.model_path.clone())
} else {
Err(crate::Error::ModelNotFound {
name: model_name.to_string(),
})
}
}
async fn list_models(&self) -> crate::Result<Vec<crate::ModelInfo>> {
let model_size = match std::fs::metadata(&self.model_path) {
Ok(metadata) => Some(metadata.len()),
Err(e) => {
warn!(
"Failed to get file size for {}: {}",
self.model_path.display(),
e
);
None
}
};
let (dimensions, max_tokens) = match crate::extract_gguf_metadata(&self.model_path) {
Ok(metadata) => {
info!(
"Successfully extracted metadata: dimensions={}, max_tokens={}",
metadata.embedding_dimensions, metadata.context_size
);
(metadata.embedding_dimensions, metadata.context_size)
}
Err(e) => {
warn!(
"Failed to extract GGUF metadata from {}: {}",
self.model_path.display(),
e
);
(0, 512)
}
};
Ok(vec![crate::ModelInfo {
name: self.model_name.clone(),
dimensions,
max_tokens,
model_size: model_size.and_then(|s| s.try_into().ok()),
}])
}
}
pub fn create_router(state: AppState) -> Router<()> {
async fn health_handler(State(state): State<AppState>) -> impl IntoResponse {
if state.is_ready() {
(
StatusCode::OK,
Json(json!({
"status": "healthy",
"model": state.model_name(),
"version": env!("CARGO_PKG_VERSION"),
})),
)
} else {
(
StatusCode::SERVICE_UNAVAILABLE,
Json(json!({
"status": "unhealthy",
"error": "Service not ready",
})),
)
}
}
Router::new()
.route("/health", get(health_handler))
.route(
"/v1/embeddings",
axum::routing::post(handlers::embeddings_handler),
)
.route("/v1/models", get(handlers::list_models_handler))
.route("/v1/rerank", axum::routing::post(handlers::rerank_handler))
.route("/cache/stats", get(cache_handlers::cache_stats_handler))
.route(
"/cache/clear",
axum::routing::post(cache_handlers::cache_clear_handler),
)
.route(
"/cache/warm",
axum::routing::post(cache_handlers::cache_warm_handler),
)
.route(
"/v1/embeddings/prefix",
axum::routing::post(cache_handlers::prefix_register_handler),
)
.route(
"/v1/embeddings/prefix",
get(cache_handlers::prefix_list_handler),
)
.route(
"/v1/embeddings/prefix",
axum::routing::delete(cache_handlers::prefix_clear_handler),
)
.route(
"/v1/embeddings/prefix/stats",
get(cache_handlers::prefix_stats_handler),
)
.layer(
tower::ServiceBuilder::new()
.layer(TraceLayer::new_for_http().make_span_with(
|request: &axum::http::Request<_>| {
let request_id = Uuid::new_v4();
tracing::info_span!(
"http_request",
request_id = %request_id,
method = %request.method(),
uri = %request.uri(),
)
},
))
.layer(CorsLayer::permissive()),
)
.with_state(state)
}
pub async fn run_server(config: ServerConfig) -> crate::Result<()> {
let model_path = &config.engine_config.model_config.model_path;
let model_name = &config.engine_config.model_config.model_name;
info!("Starting Embellama server v{}", env!("CARGO_PKG_VERSION"));
info!("Model: {} ({})", model_path.display(), model_name);
info!(
"Workers: {}, Queue size: {}",
config.worker_count, config.queue_size
);
let state = AppState::new(config.clone())?;
let app = create_router(state);
let addr: SocketAddr = format!("{}:{}", config.host, config.port)
.parse()
.map_err(|e| crate::Error::Other(anyhow::anyhow!("Invalid address: {e}")))?;
info!("Server listening on http://{}", addr);
let listener = tokio::net::TcpListener::bind(addr)
.await
.map_err(|e| crate::Error::Other(anyhow::anyhow!("Failed to bind to address: {e}")))?;
axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal())
.await
.map_err(|e| crate::Error::Other(anyhow::anyhow!("Server error: {e}")))?;
info!("Server shutdown complete");
Ok(())
}
async fn shutdown_signal() {
let ctrl_c = async {
signal::ctrl_c()
.await
.expect("Failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("Failed to install signal handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
() = ctrl_c => {
info!("Received Ctrl+C, shutting down");
}
() = terminate => {
info!("Received terminate signal, shutting down");
}
}
}