use serde::{Deserialize, Serialize};
pub mod reason {
pub const AVAILABLE: &str = "available";
pub const METAL_UNAVAILABLE: &str = "metal_unavailable";
pub const METAL_NOT_BUILT: &str = "metal_not_built";
pub const CUDA_UNAVAILABLE: &str = "cuda_unavailable";
pub const CUDA_NOT_BUILT: &str = "cuda_not_built";
pub const CPU_ONLY: &str = "cpu_only";
pub const DETECTING: &str = "detecting";
pub const DETECTION_TIMED_OUT: &str = "detection_timed_out";
pub const DISABLED: &str = "disabled";
pub const MODEL_NOT_LOADED: &str = "model_not_loaded";
}
pub mod capability {
pub const CPU: &str = "cpu";
pub const METAL: &str = "metal";
pub const CUDA: &str = "cuda";
pub const REAL_WEIGHTS: &str = "real_weights";
pub const CONTINUOUS_BATCHING: &str = "continuous_batching";
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum HealthState {
Detecting,
Ready,
Unavailable,
}
impl HealthState {
pub fn http_status(self) -> u16 {
match self {
HealthState::Detecting | HealthState::Ready => 200,
HealthState::Unavailable => 503,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Capability {
pub id: String,
pub available: bool,
pub reason: String,
pub detail: String,
}
impl Capability {
pub fn available(id: &str, detail: impl Into<String>) -> Self {
Capability {
id: id.to_string(),
available: true,
reason: reason::AVAILABLE.to_string(),
detail: detail.into(),
}
}
pub fn unavailable(id: &str, reason: &str, detail: impl Into<String>) -> Self {
Capability {
id: id.to_string(),
available: false,
reason: reason.to_string(),
detail: detail.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ModelSummary {
pub id: String,
pub tokenizer: String,
pub synthetic_weights: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct HealthResponse {
pub state: HealthState,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<ModelSummary>,
pub capabilities: Vec<Capability>,
pub version: String,
pub pid: u32,
pub uptime_seconds: f64,
pub server_time_unix_ms: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_request_age_seconds: Option<f64>,
}
impl HealthResponse {
pub fn capability(&self, id: &str) -> Option<&Capability> {
self.capabilities.iter().find(|c| c.id == id)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detecting_is_not_an_error_status() {
assert_eq!(HealthState::Detecting.http_status(), 200);
assert_eq!(HealthState::Ready.http_status(), 200);
assert_eq!(HealthState::Unavailable.http_status(), 503);
}
#[test]
fn states_serialize_as_snake_case_strings() {
let json = serde_json::to_string(&HealthState::Detecting).unwrap();
assert_eq!(json, "\"detecting\"");
}
#[test]
fn unavailable_capability_keeps_both_a_code_and_a_sentence() {
let cap = Capability::unavailable(
capability::CUDA,
reason::CUDA_NOT_BUILT,
"This build has no CUDA kernels; rebuild with --features cuda.",
);
assert!(!cap.available);
assert_eq!(cap.reason, "cuda_not_built");
assert!(cap.detail.contains("--features cuda"));
}
#[test]
fn optional_fields_are_omitted_rather_than_null() {
let health = HealthResponse {
state: HealthState::Ready,
reason: None,
detail: None,
model: None,
capabilities: vec![Capability::available(capability::CPU, "CPU kernels")],
version: "0.5.0".into(),
pid: 1,
uptime_seconds: 1.0,
server_time_unix_ms: 0,
last_request_age_seconds: None,
};
let json = serde_json::to_string(&health).unwrap();
assert!(!json.contains("null"), "{json}");
let back: HealthResponse = serde_json::from_str(&json).unwrap();
assert_eq!(back, health);
assert!(back.capability(capability::CPU).is_some());
}
}