use crate::server::api_types::{
CacheClearResponse, CacheStatsResponse, CacheWarmRequest, CacheWarmResponse, ErrorResponse,
MemoryInfo, PrefixInfo, PrefixListResponse, PrefixRegisterRequest, PrefixRegisterResponse,
PrefixStatsResponse,
};
use crate::server::state::AppState;
use axum::{Json, extract::State, http::StatusCode, response::IntoResponse};
use sha2::{Digest, Sha256};
use tracing::{debug, error, info};
fn lock_poisoned_response() -> (StatusCode, Json<ErrorResponse>) {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::internal_error("Engine lock poisoned")),
)
}
pub async fn cache_stats_handler(State(state): State<AppState>) -> impl IntoResponse {
debug!("Processing cache stats request");
let cache_stats = {
let Ok(engine) = state.engine.lock() else {
return lock_poisoned_response().into_response();
};
engine.get_cache_stats()
};
let memory = get_memory_info();
let response = CacheStatsResponse {
enabled: cache_stats.is_some(),
stats: cache_stats,
memory,
};
info!("Returning cache stats: enabled={}", response.enabled);
(StatusCode::OK, Json(response)).into_response()
}
pub async fn cache_clear_handler(State(state): State<AppState>) -> impl IntoResponse {
debug!("Processing cache clear request");
let previous_stats = {
let Ok(engine) = state.engine.lock() else {
return lock_poisoned_response().into_response();
};
let cache_stats = engine.get_cache_stats();
engine.clear_cache();
cache_stats
};
let response = CacheClearResponse {
status: "Cache cleared successfully".to_string(),
previous_stats,
};
info!("Cache cleared successfully");
(StatusCode::OK, Json(response)).into_response()
}
#[allow(clippy::too_many_lines)]
pub async fn cache_warm_handler(
State(state): State<AppState>,
Json(request): Json<CacheWarmRequest>,
) -> impl IntoResponse {
debug!(
"Processing cache warm request with {} texts",
request.texts.len()
);
if request.texts.is_empty() {
return (
StatusCode::BAD_REQUEST,
Json(ErrorResponse::invalid_request(
"No texts provided for warming",
)),
)
.into_response();
}
if request.texts.len() > 1000 {
return (
StatusCode::BAD_REQUEST,
Json(ErrorResponse::invalid_request(
"Too many texts provided (max 1000)",
)),
)
.into_response();
}
let initial_stats = {
let Ok(engine) = state.engine.lock() else {
return lock_poisoned_response().into_response();
};
engine.get_cache_stats()
};
let result = {
let Ok(engine) = state.engine.lock() else {
return lock_poisoned_response().into_response();
};
let texts: Vec<&str> = request
.texts
.iter()
.map(std::string::String::as_str)
.collect();
engine.warm_cache(request.model.as_deref(), &texts)
};
match result {
Ok(()) => {
let final_stats = {
let Ok(engine) = state.engine.lock() else {
return lock_poisoned_response().into_response();
};
engine.get_cache_stats()
};
let already_cached = if let (Some(initial), Some(final_)) = (initial_stats, final_stats)
{
let new_entries =
usize::try_from(final_.entry_count.saturating_sub(initial.entry_count))
.unwrap_or(0);
request.texts.len().saturating_sub(new_entries)
} else {
0
};
let response = CacheWarmResponse {
status: "Cache warming completed".to_string(),
texts_processed: request.texts.len(),
already_cached,
};
info!(
"Cache warmed with {} texts ({} already cached)",
request.texts.len(),
already_cached
);
(StatusCode::OK, Json(response)).into_response()
}
Err(e) => {
error!("Failed to warm cache: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::internal_error(format!(
"Failed to warm cache: {e}"
))),
)
.into_response()
}
}
}
fn get_memory_info() -> MemoryInfo {
use sysinfo::System;
let mut system = System::new();
system.refresh_memory();
let total_bytes = system.total_memory();
let available_bytes = system.available_memory();
let used_bytes = total_bytes.saturating_sub(available_bytes);
#[allow(clippy::cast_precision_loss)]
let usage_percentage = if total_bytes > 0 {
(used_bytes as f32 / total_bytes as f32) * 100.0
} else {
0.0
};
MemoryInfo {
total_bytes,
available_bytes,
usage_percentage,
}
}
pub async fn prefix_register_handler(
State(state): State<AppState>,
Json(request): Json<PrefixRegisterRequest>,
) -> impl IntoResponse {
debug!("Processing prefix register request");
if request.prefix.is_empty() {
return (
StatusCode::BAD_REQUEST,
Json(ErrorResponse::invalid_request("Prefix cannot be empty")),
)
.into_response();
}
if request.prefix.len() > 10_000 {
return (
StatusCode::BAD_REQUEST,
Json(ErrorResponse::invalid_request(
"Prefix too long (max 10,000 characters)",
)),
)
.into_response();
}
let result = {
let Ok(engine) = state.engine.lock() else {
return lock_poisoned_response().into_response();
};
engine.register_prefix(request.model.as_deref(), &request.prefix)
};
match result {
Ok(()) => {
let token_count = request.prefix.len() / 4; let memory_usage = (token_count * 1024) as u64;
let response = PrefixRegisterResponse {
status: "Prefix registered successfully".to_string(),
token_count,
memory_usage,
};
info!("Registered prefix of ~{} tokens", token_count);
(StatusCode::OK, Json(response)).into_response()
}
Err(e) => {
error!("Failed to register prefix: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse::internal_error(format!(
"Failed to register prefix: {e}"
))),
)
.into_response()
}
}
}
pub async fn prefix_list_handler(State(state): State<AppState>) -> impl IntoResponse {
debug!("Processing prefix list request");
let Ok(engine) = state.engine.lock() else {
return lock_poisoned_response().into_response();
};
let cached_prefixes = engine.list_cached_prefixes();
let prefixes: Vec<PrefixInfo> = cached_prefixes
.iter()
.map(|prefix| {
let preview = if prefix.len() > 100 {
format!("{}...", &prefix[..100])
} else {
prefix.clone()
};
PrefixInfo {
key: format!("{:x}", Sha256::digest(prefix.as_bytes())),
preview,
token_count: prefix.len() / 4, access_count: 0, age_seconds: 0, }
})
.collect();
let response = PrefixListResponse {
total_count: prefixes.len(),
prefixes,
};
(StatusCode::OK, Json(response)).into_response()
}
pub async fn prefix_clear_handler(State(state): State<AppState>) -> impl IntoResponse {
debug!("Processing prefix clear request");
{
let Ok(engine) = state.engine.lock() else {
return lock_poisoned_response().into_response();
};
engine.clear_prefix_cache();
}
let response = CacheClearResponse {
status: "Prefix cache cleared successfully".to_string(),
previous_stats: None, };
info!("Prefix cache cleared");
(StatusCode::OK, Json(response)).into_response()
}
pub async fn prefix_stats_handler(State(state): State<AppState>) -> impl IntoResponse {
debug!("Processing prefix stats request");
let Ok(engine) = state.engine.lock() else {
return lock_poisoned_response().into_response();
};
let is_enabled = engine.is_prefix_cache_enabled();
if let Some(stats) = engine.get_prefix_cache_stats() {
#[allow(clippy::cast_precision_loss)]
let hit_rate = if stats.total_hits + stats.total_misses > 0 {
stats.total_hits as f64 / (stats.total_hits + stats.total_misses) as f64
} else {
0.0
};
let response = PrefixStatsResponse {
enabled: true,
session_count: stats.session_count,
total_hits: stats.total_hits,
total_misses: stats.total_misses,
total_evictions: stats.total_evictions,
memory_usage_bytes: stats.memory_usage_bytes,
hit_rate,
};
(StatusCode::OK, Json(response)).into_response()
} else {
let response = PrefixStatsResponse {
enabled: is_enabled,
session_count: 0,
total_hits: 0,
total_misses: 0,
total_evictions: 0,
memory_usage_bytes: 0,
hit_rate: 0.0,
};
(StatusCode::OK, Json(response)).into_response()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_memory_info() {
let info = get_memory_info();
assert!(info.total_bytes > 0);
assert!(info.usage_percentage >= 0.0 && info.usage_percentage <= 100.0);
}
}