Files
rustytorch/crates/production/rtx-serving-api/src/server.rs
T
osobhandClaude Fable 5 733b02cd8b
GPU Tests / Check GPU Availability (push) Successful in 1s
CI / Build (ubuntu-latest) (push) Failing after 6s
CI / Build (macos-latest) (push) Failing after 11s
Documentation / Build User Guide (push) Successful in 7s
CI / Clippy Check (push) Failing after 21s
CI / Format Check (push) Failing after 6s
GPU Tests / CUDA Tests (11.8) (push) Has been skipped
GPU Tests / CUDA Tests (12.1) (push) Has been skipped
CI / Build CPU-Only (Explicit) (push) Failing after 6s
GPU Tests / Metal Tests (push) Has been skipped
CI / Test (macos-latest) (push) Has been skipped
CI / Test (ubuntu-latest) (push) Has been skipped
CI / Python Bindings (maturin) (macos-latest) (push) Has been skipped
CI / Python Bindings (maturin) (ubuntu-latest) (push) Has been skipped
CI / WASM Build + Size Check (push) Has been skipped
CI / Distributed Training Tests (push) Has been skipped
CI / CI Success (push) Failing after 0s
Performance Benchmarks / Run Benchmarks (push) Successful in 28s
Documentation / Build API Documentation (push) Failing after 25s
feat(inference): concrete EAGLE draft model + real tokenizer at the serving boundary
EAGLE (rtx-inference/src/eagle.rs, ~610 lines, mirrors medusa.rs
conventions): EagleDraftHead autoregressive FFN with Concat/Add/
Attention feature fusion, EagleHeads draft model with draft/
draft_steps (per-step top-k for candidate trees) and teacher-forced
training_loss; implements the speculative::EagleDraftModel trait so it
plugs into the orchestration layer. 38 unit tests.

Tokenizer (rtx-inference/src/tokenizer.rs): ServingTokenizer enum —
Vocab (HuggingFace tokenizers, loadable from tokenizer.json) or
ByteLevel fallback preserving previous behavior. rtx-serving-api's
AppState and rtx-streaming's token generator now encode/decode through
it (with_engine_and_tokenizer / set_tokenizer added; existing
signatures unchanged). Also fixes two pre-existing compile errors in
rtx-streaming (missing import, stray .await) that blocked its lib
tests entirely.

Tests: rtx-inference 328 pass, rtx-serving-api 193 pass, rtx-streaming
53 pass (2 pre-existing mock-server connection failures unrelated to
these changes).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-09 21:54:05 -07:00

149 lines
4.5 KiB
Rust

//! HTTP server configuration and startup
use axum::{
Router,
routing::{get, post},
};
use std::time::Duration;
use tokio::net::TcpListener;
use tower_http::{cors::CorsLayer, timeout::TimeoutLayer, trace::TraceLayer};
use crate::{ApiError, ApiResult, health, inference, models};
/// Server configuration
#[derive(Debug, Clone)]
pub struct ServerConfig {
/// Listen address
pub host: String,
/// Listen port
pub port: u16,
/// Request timeout
pub timeout_seconds: u64,
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
host: "127.0.0.1".to_string(),
port: 8080,
timeout_seconds: 30,
}
}
}
/// Serving server
pub struct ServingServer {
config: ServerConfig,
state: inference::AppState,
}
impl ServingServer {
/// Create a new serving server (no engine loaded; inference returns 503)
#[must_use]
pub fn new(config: ServerConfig) -> Self {
Self {
config,
state: inference::AppState::default(),
}
}
/// Create a serving server backed by a live inference engine.
///
/// Uses byte-level tokenization by default; use
/// [`ServingServer::with_engine_and_tokenizer`] to attach a real
/// vocabulary tokenizer.
#[must_use]
pub fn with_engine(
config: ServerConfig,
engine: std::sync::Arc<tokio::sync::RwLock<rtx_inference::InferenceEngine>>,
) -> Self {
Self::with_engine_and_tokenizer(config, engine, rtx_inference::ServingTokenizer::default())
}
/// Create a serving server backed by a live inference engine and an
/// explicit tokenizer (e.g. loaded via
/// `rtx_inference::ServingTokenizer::from_file`).
#[must_use]
pub fn with_engine_and_tokenizer(
config: ServerConfig,
engine: std::sync::Arc<tokio::sync::RwLock<rtx_inference::InferenceEngine>>,
tokenizer: rtx_inference::ServingTokenizer,
) -> Self {
Self {
config,
state: inference::AppState {
engine: Some(engine),
tokenizer,
},
}
}
/// Build the Axum router with all routes
pub fn app(&self) -> Router {
Router::new()
// Health endpoints
.route("/health", get(health::health_check))
.route("/health/ready", get(health::health_check))
.route("/health/live", get(health::health_check))
// Model endpoints
.route("/v1/models", get(models::list_models))
// Inference endpoints
.route("/v1/completions", post(inference::inference))
.route("/v1/chat/completions", post(inference::inference))
// Enhanced cached inference endpoints (temporarily disabled)
// .route("/v1/cached/completions", post(inference_cached::cached_inference))
// .route("/v1/cached/chat/completions", post(inference_cached::cached_inference))
// Cache management endpoints (temporarily disabled)
// .route("/v1/cache/stats", get(inference_cached::cache_stats))
// .route("/v1/cache/clear", post(inference_cached::clear_cache))
// .route("/v1/cache/warm", post(inference_cached::warm_cache))
// Middleware
.layer(CorsLayer::permissive())
.layer(TraceLayer::new_for_http())
.layer(TimeoutLayer::new(Duration::from_secs(
self.config.timeout_seconds,
)))
.with_state(self.state.clone())
}
/// Start the server
pub async fn serve(&self) -> ApiResult<()> {
let app = self.app();
let addr = format!("{}:{}", self.config.host, self.config.port);
let listener = TcpListener::bind(&addr)
.await
.map_err(|e| ApiError::internal(format!("Failed to bind to {addr}: {e}")))?;
tracing::info!("Server listening on {}", addr);
axum::serve(listener, app)
.await
.map_err(|e| ApiError::internal(format!("Server error: {e}")))?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_server_config_default() {
let config = ServerConfig::default();
assert_eq!(config.host, "127.0.0.1");
assert_eq!(config.port, 8080);
assert_eq!(config.timeout_seconds, 30);
}
#[test]
fn test_server_creation() {
let config = ServerConfig::default();
let server = ServingServer::new(config);
// Should be able to build the app router
let _app = server.app();
}
}