use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize)]
pub struct ApiError {
pub error: ApiErrorBody,
#[serde(skip)]
pub status: StatusCode,
#[serde(skip)]
pub retry_after_seconds: Option<u64>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ApiErrorBody {
pub message: String,
#[serde(rename = "type")]
pub error_type: String,
pub param: Option<String>,
pub code: Option<String>,
}
impl ApiError {
fn bare(
status: StatusCode,
message: impl Into<String>,
error_type: &str,
code: Option<&str>,
param: Option<String>,
) -> Self {
Self {
status,
retry_after_seconds: None,
error: ApiErrorBody {
message: message.into(),
error_type: error_type.into(),
param,
code: code.map(String::from),
},
}
}
pub fn invalid_request(message: impl Into<String>, param: Option<String>) -> Self {
Self::bare(
StatusCode::BAD_REQUEST,
message,
"invalid_request_error",
None,
param,
)
}
pub fn model_not_found(model_name: &str) -> Self {
Self::bare(
StatusCode::NOT_FOUND,
format!("The model '{}' does not exist", model_name),
"invalid_request_error",
Some("model_not_found"),
Some("model".into()),
)
}
pub fn model_not_loaded(model_name: &str) -> Self {
Self::bare(
StatusCode::BAD_REQUEST,
format!("The model '{}' is cached but not currently loaded. Start the server with `--model <path>` for this model.", model_name),
"invalid_request_error",
Some("model_not_loaded"),
Some("model".into()),
)
}
pub fn context_length_exceeded(max_tokens: usize, actual_tokens: usize) -> Self {
Self::bare(
StatusCode::BAD_REQUEST,
format!(
"This model's maximum context length is {} tokens. However, your messages resulted in {} tokens.",
max_tokens, actual_tokens
),
"invalid_request_error",
Some("context_length_exceeded"),
Some("messages".into()),
)
}
pub fn queue_full() -> Self {
let mut e = Self::bare(
StatusCode::TOO_MANY_REQUESTS,
"Server is at capacity. Too many pending requests.",
"server_error",
Some("queue_full"),
None,
);
e.retry_after_seconds = Some(1);
e
}
pub fn slot_budget_exceeded(needed_bytes: u64, budget_bytes: u64) -> Self {
let mut e = Self::bare(
StatusCode::TOO_MANY_REQUESTS,
format!(
"Per-slot KV cache budget exceeded for this request \
(needed_bytes={}, budget_bytes={}). Reduce `max_tokens` \
or send a shorter prompt; the per-slot KV budget is \
derived from `kv_cache_budget_bytes / max_slots` \
(ADR-040 §3.5).",
needed_bytes, budget_bytes
),
"server_error",
Some("slot_budget_exceeded"),
None,
);
e.retry_after_seconds = Some(1);
e
}
pub fn not_ready() -> Self {
let mut e = Self::bare(
StatusCode::SERVICE_UNAVAILABLE,
"Model is still warming up; please retry shortly.",
"server_error",
Some("not_ready"),
None,
);
e.retry_after_seconds = Some(1);
e
}
pub fn generation_error(detail: impl Into<String>) -> Self {
Self::bare(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Generation failed: {}", detail.into()),
"server_error",
Some("generation_error"),
None,
)
}
pub fn internal_error() -> Self {
Self::bare(
StatusCode::INTERNAL_SERVER_ERROR,
"Internal server error",
"server_error",
Some("internal_error"),
None,
)
}
pub fn unauthorized() -> Self {
Self::bare(
StatusCode::UNAUTHORIZED,
"Missing or invalid authorization header.",
"authentication_error",
Some("invalid_api_key"),
None,
)
}
pub fn no_mmproj_loaded() -> Self {
Self::bare(
StatusCode::BAD_REQUEST,
"Request includes image_url content parts but this server \
was started without a multimodal projector. Start with \
`--mmproj <path>` or send a text-only request.",
"invalid_request_error",
Some("no_mmproj_loaded"),
Some("messages".into()),
)
}
pub fn grammar_error(detail: impl Into<String>) -> Self {
Self::bare(
StatusCode::BAD_REQUEST,
format!("Grammar compilation failed: {}", detail.into()),
"invalid_request_error",
Some("grammar_error"),
Some("response_format".into()),
)
}
pub fn not_found(message: impl Into<String>) -> Self {
Self::bare(
StatusCode::NOT_FOUND,
message,
"invalid_request_error",
None,
None,
)
}
pub fn not_implemented(message: impl Into<String>) -> Self {
Self::bare(
StatusCode::NOT_IMPLEMENTED,
message,
"server_error",
Some("not_implemented"),
None,
)
}
pub fn capability_unsupported(capability: &str) -> Self {
Self::bare(
StatusCode::NOT_IMPLEMENTED,
format!(
"Capability not yet implemented: {capability} \
(ADR-040 §6 Phase C C3 — MultiSeqKvCache::* unimplemented per-model)"
),
"server_error",
Some("capability_unsupported"),
None,
)
}
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
use axum::http::{header, HeaderValue};
let status = self.status;
let retry_after = self.retry_after_seconds;
let body = serde_json::to_string(&self).unwrap_or_else(|_| {
r#"{"error":{"message":"Internal serialization error","type":"server_error","param":null,"code":null}}"#.into()
});
let mut response =
(status, [(header::CONTENT_TYPE, "application/json")], body).into_response();
if let Some(secs) = retry_after {
if let Ok(val) = HeaderValue::from_str(&secs.to_string()) {
response.headers_mut().insert(header::RETRY_AFTER, val);
}
}
response
}
}
#[derive(Debug, Clone, Serialize)]
pub struct HealthResponse {
pub status: String,
pub model: Option<String>,
pub backend: &'static str,
pub context_length: Option<usize>,
pub uptime_seconds: u64,
}
#[derive(Debug, Clone, Serialize)]
pub struct ReadyzResponse {
pub ready: bool,
pub detail: &'static str,
}
#[derive(Debug, Clone, Serialize)]
pub struct ModelObject {
pub id: String,
pub object: &'static str,
pub created: i64,
pub owned_by: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
pub context_length: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub quant_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub backend: Option<&'static str>,
pub loaded: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub arch: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_context_length: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub provenance: Option<&'static str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub moe_experts: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub moe_experts_per_tok: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sliding_window: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub kv_spill_active: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub quant_bpw: Option<f32>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ModelListResponse {
pub object: &'static str,
pub data: Vec<ModelObject>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ChatMessage {
pub role: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content: Option<MessageContent>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning_content: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCall>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum MessageContent {
Text(String),
Parts(Vec<ContentPart>),
}
impl MessageContent {
pub fn text(&self) -> String {
match self {
MessageContent::Text(s) => s.clone(),
MessageContent::Parts(parts) => parts
.iter()
.filter_map(|p| match p {
ContentPart::Text { text } => Some(text.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join(""),
}
}
pub fn image_urls(&self) -> Vec<&str> {
match self {
MessageContent::Text(_) => Vec::new(),
MessageContent::Parts(parts) => parts
.iter()
.filter_map(|p| match p {
ContentPart::ImageUrl { image_url } => Some(image_url.url.as_str()),
_ => None,
})
.collect(),
}
}
pub fn has_images(&self) -> bool {
match self {
MessageContent::Text(_) => false,
MessageContent::Parts(parts) => parts
.iter()
.any(|p| matches!(p, ContentPart::ImageUrl { .. })),
}
}
pub fn as_text_opt(&self) -> Option<String> {
let text = self.text();
if text.is_empty() {
None
} else {
Some(text)
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type")]
pub enum ContentPart {
#[serde(rename = "text")]
Text { text: String },
#[serde(rename = "image_url")]
ImageUrl { image_url: ImageUrl },
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ImageUrl {
pub url: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ToolCall {
pub id: String,
#[serde(rename = "type")]
pub call_type: String,
pub function: ToolCallFunction,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ToolCallFunction {
pub name: String,
pub arguments: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tool {
#[serde(rename = "type")]
pub tool_type: String,
pub function: ToolFunction,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolFunction {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parameters: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum StopSequence {
Single(String),
Multiple(Vec<String>),
}
impl StopSequence {
pub fn into_vec(self) -> Vec<String> {
match self {
StopSequence::Single(s) => vec![s],
StopSequence::Multiple(v) => v,
}
}
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(tag = "type")]
pub enum ResponseFormat {
#[serde(rename = "text")]
Text,
#[serde(rename = "json_object")]
JsonObject,
#[serde(rename = "json_schema")]
JsonSchema { json_schema: JsonSchemaSpec },
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
pub struct JsonSchemaSpec {
pub name: String,
#[serde(default)]
pub description: Option<String>,
pub schema: serde_json::Value,
#[serde(default)]
pub strict: Option<bool>,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Default)]
pub struct StreamOptions {
#[serde(default)]
pub include_usage: Option<bool>,
}
pub type LogitBiasMap = std::collections::HashMap<String, f32>;
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum OverflowPolicy {
Reject,
TruncateLeft,
#[default]
Summarize,
}
#[derive(Debug, Clone, Deserialize)]
#[allow(dead_code)]
pub struct ChatCompletionRequest {
pub model: String,
pub messages: Vec<ChatMessage>,
#[serde(default)]
pub stream: Option<bool>,
#[serde(default)]
pub max_tokens: Option<usize>,
#[serde(default)]
pub max_completion_tokens: Option<usize>,
#[serde(default)]
pub temperature: Option<f32>,
#[serde(default)]
pub stop: Option<StopSequence>,
#[serde(default)]
pub tools: Option<Vec<Tool>>,
#[serde(default)]
pub tool_choice: Option<serde_json::Value>,
#[serde(default)]
pub response_format: Option<ResponseFormat>,
#[serde(default)]
pub top_p: Option<f32>,
#[serde(default)]
pub seed: Option<u64>,
#[serde(default)]
pub frequency_penalty: Option<f32>,
#[serde(default)]
pub presence_penalty: Option<f32>,
#[serde(default)]
pub stream_options: Option<StreamOptions>,
#[serde(default)]
pub top_k: Option<u32>,
#[serde(default)]
pub repetition_penalty: Option<f32>,
#[serde(default)]
pub min_p: Option<f32>,
#[serde(default)]
pub logprobs: Option<bool>,
#[serde(default)]
pub top_logprobs: Option<u32>,
#[serde(default)]
pub logit_bias: Option<LogitBiasMap>,
#[serde(default)]
pub parallel_tool_calls: Option<bool>,
#[serde(default)]
pub hf2q_overflow_policy: Option<OverflowPolicy>,
#[serde(default)]
pub hf2q_enable_thinking: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub chat_template_kwargs: Option<serde_json::Map<String, serde_json::Value>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TimingInfo {
pub prefill_time_secs: f64,
pub decode_time_secs: f64,
pub total_time_secs: f64,
pub time_to_first_token_ms: f64,
pub prefill_tokens_per_sec: f64,
pub decode_tokens_per_sec: f64,
pub gpu_sync_count: u64,
pub gpu_dispatch_count: u64,
}
#[derive(Debug, Clone, Serialize)]
pub struct ChatCompletionResponse {
pub id: String,
pub object: &'static str,
pub created: i64,
pub model: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub system_fingerprint: Option<String>,
pub choices: Vec<ChatCompletionChoice>,
pub usage: UsageStats,
#[serde(skip_serializing_if = "Option::is_none")]
pub x_hf2q_timing: Option<TimingInfo>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ChatCompletionChoice {
pub index: usize,
pub message: ChatMessage,
pub finish_reason: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub logprobs: Option<ChoiceLogprobs>,
}
#[derive(Debug, Clone, Serialize)]
pub struct UsageStats {
pub prompt_tokens: usize,
pub completion_tokens: usize,
pub total_tokens: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt_tokens_details: Option<PromptTokensDetails>,
#[serde(skip_serializing_if = "Option::is_none")]
pub completion_tokens_details: Option<CompletionTokensDetails>,
}
#[derive(Debug, Clone, Serialize)]
pub struct PromptTokensDetails {
pub cached_tokens: usize,
}
#[derive(Debug, Clone, Serialize)]
pub struct CompletionTokensDetails {
pub reasoning_tokens: usize,
}
#[derive(Debug, Clone, Serialize)]
pub struct ChoiceLogprobs {
pub content: Vec<TokenLogprob>,
}
#[derive(Debug, Clone, Serialize)]
pub struct TokenLogprob {
pub token: String,
pub logprob: f32,
pub bytes: Option<Vec<u8>>,
pub top_logprobs: Vec<TopLogprobEntry>,
}
#[derive(Debug, Clone, Serialize)]
pub struct TopLogprobEntry {
pub token: String,
pub logprob: f32,
pub bytes: Option<Vec<u8>>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ChatCompletionChunk {
pub id: String,
pub object: &'static str,
pub created: i64,
pub model: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub system_fingerprint: Option<String>,
pub choices: Vec<ChunkChoice>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<UsageStats>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ChunkChoice {
pub index: usize,
pub delta: ChunkDelta,
pub finish_reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub logprobs: Option<ChoiceLogprobs>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ChunkDelta {
#[serde(skip_serializing_if = "Option::is_none")]
pub role: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning_content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCallDelta>>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ToolCallDelta {
pub index: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub call_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub function: Option<ToolCallFunctionDelta>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ToolCallFunctionDelta {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ToolChoiceValue {
Auto,
None,
Required,
Function(String),
}
impl ToolChoiceValue {
pub fn parse(value: Option<&serde_json::Value>) -> Self {
match value {
None => ToolChoiceValue::Auto,
Some(serde_json::Value::String(s)) => match s.as_str() {
"none" => ToolChoiceValue::None,
"required" => ToolChoiceValue::Required,
_ => ToolChoiceValue::Auto,
},
Some(serde_json::Value::Object(obj)) => {
if let Some(func_obj) = obj.get("function") {
if let Some(name) = func_obj.get("name").and_then(|n| n.as_str()) {
return ToolChoiceValue::Function(name.to_string());
}
}
ToolChoiceValue::Auto
}
_ => ToolChoiceValue::Auto,
}
}
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum EmbeddingInput {
Single(String),
Multiple(Vec<String>),
}
impl EmbeddingInput {
pub fn into_vec(self) -> Vec<String> {
match self {
EmbeddingInput::Single(s) => vec![s],
EmbeddingInput::Multiple(v) => v,
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[allow(dead_code)]
pub struct EmbeddingRequest {
pub model: String,
pub input: EmbeddingInput,
#[serde(default)]
pub encoding_format: Option<String>,
#[serde(default)]
pub dimensions: Option<usize>,
#[serde(default)]
pub user: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(untagged)]
pub enum EmbeddingPayload {
Float(Vec<f32>),
Base64(String),
}
#[derive(Debug, Clone, Serialize)]
pub struct EmbeddingObject {
pub object: &'static str,
pub embedding: EmbeddingPayload,
pub index: usize,
}
#[derive(Debug, Clone, Serialize)]
pub struct EmbeddingResponse {
pub object: &'static str,
pub data: Vec<EmbeddingObject>,
pub model: String,
pub usage: EmbeddingUsage,
}
#[derive(Debug, Clone, Serialize)]
pub struct EmbeddingUsage {
pub prompt_tokens: usize,
pub total_tokens: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_api_error_serialization() {
let err = ApiError::invalid_request("Something went wrong", None);
let json = serde_json::to_value(&err).unwrap();
assert_eq!(json["error"]["message"], "Something went wrong");
assert_eq!(json["error"]["type"], "invalid_request_error");
assert!(json["error"]["param"].is_null());
assert!(json["error"]["code"].is_null());
}
#[test]
fn test_api_error_with_param() {
let err = ApiError::invalid_request("Bad field", Some("messages".into()));
let json = serde_json::to_value(&err).unwrap();
assert_eq!(json["error"]["param"], "messages");
}
#[test]
fn test_model_not_found_error() {
let err = ApiError::model_not_found("gpt-5");
let json = serde_json::to_value(&err).unwrap();
assert_eq!(json["error"]["code"], "model_not_found");
assert!(json["error"]["message"].as_str().unwrap().contains("gpt-5"));
assert_eq!(err.status, StatusCode::NOT_FOUND);
}
#[test]
fn test_model_not_loaded_error() {
let err = ApiError::model_not_loaded("qwen3.6-27b");
assert_eq!(err.status, StatusCode::BAD_REQUEST);
assert_eq!(err.error.code.as_deref(), Some("model_not_loaded"));
assert!(err.error.message.contains("qwen3.6-27b"));
assert_eq!(err.error.param.as_deref(), Some("model"));
}
#[test]
fn test_context_length_exceeded_error() {
let err = ApiError::context_length_exceeded(8192, 9000);
let json = serde_json::to_value(&err).unwrap();
assert_eq!(json["error"]["code"], "context_length_exceeded");
let msg = json["error"]["message"].as_str().unwrap();
assert!(msg.contains("8192"));
assert!(msg.contains("9000"));
assert_eq!(err.status, StatusCode::BAD_REQUEST);
}
#[test]
fn test_queue_full_error_is_429_with_retry_after() {
let err = ApiError::queue_full();
let response = err.into_response();
assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS);
assert_eq!(
response
.headers()
.get("retry-after")
.and_then(|v| v.to_str().ok()),
Some("1")
);
}
#[test]
fn test_not_ready_is_503_with_retry_after() {
let err = ApiError::not_ready();
let response = err.into_response();
assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
assert_eq!(
response
.headers()
.get("retry-after")
.and_then(|v| v.to_str().ok()),
Some("1")
);
}
#[test]
fn test_unauthorized_error() {
let err = ApiError::unauthorized();
assert_eq!(err.status, StatusCode::UNAUTHORIZED);
assert_eq!(err.error.error_type, "authentication_error");
}
#[test]
fn test_grammar_error() {
let err = ApiError::grammar_error("unclosed brace at pos 42");
let json = serde_json::to_value(&err).unwrap();
assert_eq!(err.status, StatusCode::BAD_REQUEST);
assert_eq!(json["error"]["code"], "grammar_error");
assert_eq!(json["error"]["param"], "response_format");
assert!(json["error"]["message"]
.as_str()
.unwrap()
.contains("unclosed brace at pos 42"));
}
#[test]
fn test_generation_error() {
let err = ApiError::generation_error("Metal command buffer error");
let json = serde_json::to_value(&err).unwrap();
assert_eq!(json["error"]["code"], "generation_error");
assert!(json["error"]["message"]
.as_str()
.unwrap()
.contains("Metal command buffer error"));
assert_eq!(err.status, StatusCode::INTERNAL_SERVER_ERROR);
}
#[test]
fn test_internal_error() {
let err = ApiError::internal_error();
assert_eq!(err.status, StatusCode::INTERNAL_SERVER_ERROR);
assert_eq!(err.error.code, Some("internal_error".into()));
}
#[test]
fn test_model_list_response_serialization() {
let resp = ModelListResponse {
object: "list",
data: vec![ModelObject {
id: "test-model".into(),
object: "model",
created: 1234567890,
owned_by: "hf2q",
context_length: Some(262144),
quant_type: Some("Q4_K_M".into()),
backend: Some("mlx-native"),
loaded: true,
arch: None,
max_context_length: None,
provenance: None,
moe_experts: None,
moe_experts_per_tok: None,
sliding_window: None,
kv_spill_active: None,
quant_bpw: None,
}],
};
let json = serde_json::to_value(&resp).unwrap();
assert_eq!(json["object"], "list");
assert_eq!(json["data"][0]["id"], "test-model");
assert_eq!(json["data"][0]["object"], "model");
assert_eq!(json["data"][0]["created"], 1234567890);
assert_eq!(json["data"][0]["owned_by"], "hf2q");
assert_eq!(json["data"][0]["context_length"], 262144);
assert_eq!(json["data"][0]["quant_type"], "Q4_K_M");
assert_eq!(json["data"][0]["backend"], "mlx-native");
assert_eq!(json["data"][0]["loaded"], true);
let entry = &json["data"][0];
assert!(entry.get("arch").is_none(), "arch must be skipped");
assert!(
entry.get("max_context_length").is_none(),
"max_context_length must be skipped"
);
assert!(
entry.get("provenance").is_none(),
"provenance must be skipped"
);
assert!(
entry.get("moe_experts").is_none(),
"moe_experts must be skipped"
);
assert!(
entry.get("moe_experts_per_tok").is_none(),
"moe_experts_per_tok must be skipped"
);
assert!(
entry.get("sliding_window").is_none(),
"sliding_window must be skipped"
);
assert!(
entry.get("kv_spill_active").is_none(),
"kv_spill_active must be skipped"
);
assert!(
entry.get("quant_bpw").is_none(),
"quant_bpw must be skipped"
);
}
#[test]
fn test_model_object_with_load_info_fields() {
let obj = ModelObject {
id: "Qwen3.6-27B-A3B-DWQ46-MoE".into(),
object: "model",
created: 1700000000,
owned_by: "hf2q",
context_length: Some(262_144),
quant_type: Some("Q4_K".into()),
backend: Some("mlx-native"),
loaded: true,
arch: Some("qwen35moe".into()),
max_context_length: Some(262_144),
provenance: Some("hf2q"),
moe_experts: Some(128),
moe_experts_per_tok: Some(8),
sliding_window: None,
kv_spill_active: Some(false),
quant_bpw: Some(4.55),
};
let json = serde_json::to_value(&obj).unwrap();
assert_eq!(json["arch"], "qwen35moe");
assert_eq!(json["max_context_length"], 262_144);
assert_eq!(json["provenance"], "hf2q");
assert_eq!(json["moe_experts"], 128);
assert_eq!(json["moe_experts_per_tok"], 8);
assert!(
json.get("sliding_window").is_none(),
"sliding_window=None must be skipped"
);
assert_eq!(json["kv_spill_active"], false);
let bpw = json["quant_bpw"].as_f64().expect("quant_bpw f64");
assert!(
(bpw - 4.55_f64).abs() < 1e-3,
"quant_bpw expected ≈4.55, got {bpw}"
);
}
#[test]
fn test_health_response_serialization() {
let resp = HealthResponse {
status: "ok".into(),
model: Some("gemma4-26b".into()),
backend: "mlx-native",
context_length: Some(262144),
uptime_seconds: 42,
};
let json = serde_json::to_value(&resp).unwrap();
assert_eq!(json["status"], "ok");
assert_eq!(json["model"], "gemma4-26b");
assert_eq!(json["backend"], "mlx-native");
assert_eq!(json["uptime_seconds"], 42);
}
#[test]
fn test_readyz_response_serialization() {
let resp = ReadyzResponse {
ready: false,
detail: "warming up",
};
let json = serde_json::to_value(&resp).unwrap();
assert_eq!(json["ready"], false);
assert_eq!(json["detail"], "warming up");
}
#[test]
fn test_chat_completion_response_serialization() {
let resp = ChatCompletionResponse {
id: "chatcmpl-123".into(),
object: "chat.completion",
created: 1700000000,
model: "test-model".into(),
system_fingerprint: Some("hf2q-deadbeef-mlx-native".into()),
choices: vec![ChatCompletionChoice {
index: 0,
message: ChatMessage {
role: "assistant".into(),
content: Some(MessageContent::Text("Hello!".into())),
reasoning_content: None,
tool_calls: None,
tool_call_id: None,
name: None,
},
finish_reason: "stop".into(),
logprobs: None,
}],
usage: UsageStats {
prompt_tokens: 10,
completion_tokens: 5,
total_tokens: 15,
prompt_tokens_details: None,
completion_tokens_details: None,
},
x_hf2q_timing: None,
};
let json = serde_json::to_value(&resp).unwrap();
assert_eq!(json["id"], "chatcmpl-123");
assert_eq!(json["object"], "chat.completion");
assert_eq!(json["system_fingerprint"], "hf2q-deadbeef-mlx-native");
assert_eq!(json["choices"][0]["message"]["role"], "assistant");
assert_eq!(json["choices"][0]["message"]["content"], "Hello!");
assert_eq!(json["choices"][0]["finish_reason"], "stop");
assert_eq!(json["usage"]["prompt_tokens"], 10);
assert_eq!(json["usage"]["completion_tokens"], 5);
assert_eq!(json["usage"]["total_tokens"], 15);
}
#[test]
fn test_chat_completion_request_all_tiers_deserialize() {
let json = r#"{
"model": "gemma4-26b",
"messages": [{"role": "user", "content": "hi"}],
"stream": true,
"max_tokens": 100,
"max_completion_tokens": 200,
"temperature": 0.7,
"stop": "END",
"response_format": {"type": "json_object"},
"top_p": 0.9,
"seed": 42,
"frequency_penalty": 0.1,
"presence_penalty": 0.2,
"stream_options": {"include_usage": true},
"top_k": 40,
"repetition_penalty": 1.05,
"min_p": 0.05,
"logprobs": true,
"top_logprobs": 5,
"logit_bias": {"1234": -100.0, "5678": 100.0},
"parallel_tool_calls": false,
"hf2q_overflow_policy": "summarize"
}"#;
let req: ChatCompletionRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.model, "gemma4-26b");
assert_eq!(req.max_tokens, Some(100));
assert_eq!(req.max_completion_tokens, Some(200));
assert_eq!(req.temperature, Some(0.7));
assert!(matches!(
req.response_format,
Some(ResponseFormat::JsonObject)
));
assert_eq!(req.top_p, Some(0.9));
assert_eq!(req.seed, Some(42));
assert_eq!(req.frequency_penalty, Some(0.1));
assert_eq!(req.presence_penalty, Some(0.2));
assert_eq!(
req.stream_options.as_ref().unwrap().include_usage,
Some(true)
);
assert_eq!(req.top_k, Some(40));
assert_eq!(req.repetition_penalty, Some(1.05));
assert_eq!(req.min_p, Some(0.05));
assert_eq!(req.logprobs, Some(true));
assert_eq!(req.top_logprobs, Some(5));
assert_eq!(req.logit_bias.as_ref().unwrap().len(), 2);
assert_eq!(req.parallel_tool_calls, Some(false));
assert_eq!(req.hf2q_overflow_policy, Some(OverflowPolicy::Summarize));
}
#[test]
fn test_response_format_json_schema_deserialize() {
let json = r#"{
"model": "m",
"messages": [{"role": "user", "content": "hi"}],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "answer",
"description": "A typed answer",
"schema": {"type": "object", "properties": {"x": {"type": "integer"}}, "required": ["x"]},
"strict": true
}
}
}"#;
let req: ChatCompletionRequest = serde_json::from_str(json).unwrap();
match req.response_format {
Some(ResponseFormat::JsonSchema { json_schema }) => {
assert_eq!(json_schema.name, "answer");
assert_eq!(json_schema.description.as_deref(), Some("A typed answer"));
assert_eq!(json_schema.strict, Some(true));
assert!(json_schema.schema.is_object());
}
other => panic!("expected JsonSchema, got {:?}", other),
}
}
#[test]
fn test_chat_completion_request_minimal() {
let json = r#"{"model":"m","messages":[{"role":"user","content":"hi"}]}"#;
let req: ChatCompletionRequest = serde_json::from_str(json).unwrap();
assert!(req.stream.is_none());
assert!(req.temperature.is_none());
assert!(req.top_p.is_none());
assert!(req.max_tokens.is_none());
assert!(req.max_completion_tokens.is_none());
assert!(req.stop.is_none());
assert!(req.response_format.is_none());
assert!(req.seed.is_none());
assert!(req.top_k.is_none());
assert!(req.logprobs.is_none());
assert!(req.hf2q_overflow_policy.is_none());
}
#[test]
fn test_stop_sequence_single() {
let json = r#"{"model":"m","messages":[{"role":"user","content":"hi"}],"stop":"END"}"#;
let req: ChatCompletionRequest = serde_json::from_str(json).unwrap();
let stops = req.stop.unwrap().into_vec();
assert_eq!(stops, vec!["END"]);
}
#[test]
fn test_stop_sequence_multiple() {
let json = r#"{"model":"m","messages":[{"role":"user","content":"hi"}],"stop":["A","B"]}"#;
let req: ChatCompletionRequest = serde_json::from_str(json).unwrap();
let stops = req.stop.unwrap().into_vec();
assert_eq!(stops, vec!["A", "B"]);
}
#[test]
fn test_final_chunk_with_usage() {
let chunk = ChatCompletionChunk {
id: "chatcmpl-789".into(),
object: "chat.completion.chunk",
created: 1700000000,
model: "test-model".into(),
system_fingerprint: None,
choices: vec![ChunkChoice {
index: 0,
delta: ChunkDelta {
role: None,
content: None,
reasoning_content: None,
tool_calls: None,
},
finish_reason: Some("stop".into()),
logprobs: None,
}],
usage: Some(UsageStats {
prompt_tokens: 10,
completion_tokens: 20,
total_tokens: 30,
prompt_tokens_details: None,
completion_tokens_details: None,
}),
};
let json = serde_json::to_value(&chunk).unwrap();
assert_eq!(json["choices"][0]["finish_reason"], "stop");
assert_eq!(json["usage"]["total_tokens"], 30);
}
#[test]
fn test_tool_choice_parse_auto() {
assert_eq!(ToolChoiceValue::parse(None), ToolChoiceValue::Auto);
let val = serde_json::json!("auto");
assert_eq!(ToolChoiceValue::parse(Some(&val)), ToolChoiceValue::Auto);
}
#[test]
fn test_tool_choice_parse_none() {
let val = serde_json::json!("none");
assert_eq!(ToolChoiceValue::parse(Some(&val)), ToolChoiceValue::None);
}
#[test]
fn test_tool_choice_parse_required() {
let val = serde_json::json!("required");
assert_eq!(
ToolChoiceValue::parse(Some(&val)),
ToolChoiceValue::Required
);
}
#[test]
fn test_tool_choice_parse_forced_function() {
let val = serde_json::json!({"type": "function", "function": {"name": "get_weather"}});
match ToolChoiceValue::parse(Some(&val)) {
ToolChoiceValue::Function(name) => assert_eq!(name, "get_weather"),
other => panic!("Expected Function, got {:?}", other),
}
}
#[test]
fn test_tool_call_delta_serialization() {
let delta = ToolCallDelta {
index: 0,
id: Some("call_abc123".to_string()),
call_type: Some("function".to_string()),
function: Some(ToolCallFunctionDelta {
name: Some("get_weather".to_string()),
arguments: None,
}),
};
let json = serde_json::to_value(&delta).unwrap();
assert_eq!(json["index"], 0);
assert_eq!(json["id"], "call_abc123");
assert_eq!(json["type"], "function");
assert_eq!(json["function"]["name"], "get_weather");
assert!(json["function"].get("arguments").is_none());
}
#[test]
fn test_chat_message_with_tool_call_id() {
let json = r#"{"role":"tool","content":"sunny","tool_call_id":"call_123"}"#;
let msg: ChatMessage = serde_json::from_str(json).unwrap();
assert_eq!(msg.role, "tool");
assert_eq!(
msg.content.as_ref().map(|c| c.text()).as_deref(),
Some("sunny")
);
assert_eq!(msg.tool_call_id.as_deref(), Some("call_123"));
}
#[test]
fn test_chat_message_reasoning_content_round_trip() {
let msg = ChatMessage {
role: "assistant".into(),
content: Some(MessageContent::Text("final answer".into())),
reasoning_content: Some("let me think step by step...".into()),
tool_calls: None,
tool_call_id: None,
name: None,
};
let json = serde_json::to_value(&msg).unwrap();
assert_eq!(json["reasoning_content"], "let me think step by step...");
assert_eq!(json["content"], "final answer");
let round_trip: ChatMessage = serde_json::from_value(json).unwrap();
assert_eq!(round_trip, msg);
}
#[test]
fn test_chunk_delta_with_reasoning_only() {
let delta = ChunkDelta {
role: None,
content: None,
reasoning_content: Some("wait...".into()),
tool_calls: None,
};
let json = serde_json::to_value(&delta).unwrap();
assert!(json.get("content").is_none());
assert_eq!(json["reasoning_content"], "wait...");
}
#[test]
fn test_embedding_input_single_string() {
let json = r#""hello world""#;
let input: EmbeddingInput = serde_json::from_str(json).unwrap();
assert_eq!(input.into_vec(), vec!["hello world"]);
}
#[test]
fn test_embedding_input_array() {
let json = r#"["hello", "world"]"#;
let input: EmbeddingInput = serde_json::from_str(json).unwrap();
assert_eq!(input.into_vec(), vec!["hello", "world"]);
}
#[test]
fn test_embedding_request_deserialize() {
let json = r#"{"model": "gemma4", "input": "test input"}"#;
let req: EmbeddingRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.model, "gemma4");
assert!(matches!(req.input, EmbeddingInput::Single(_)));
assert!(req.encoding_format.is_none());
assert!(req.dimensions.is_none());
}
#[test]
fn test_embedding_response_schema() {
let resp = EmbeddingResponse {
object: "list",
data: vec![
EmbeddingObject {
object: "embedding",
embedding: EmbeddingPayload::Float(vec![0.1, 0.2]),
index: 0,
},
EmbeddingObject {
object: "embedding",
embedding: EmbeddingPayload::Float(vec![0.3, 0.4]),
index: 1,
},
],
model: "test".to_string(),
usage: EmbeddingUsage {
prompt_tokens: 10,
total_tokens: 10,
},
};
let json = serde_json::to_value(&resp).unwrap();
assert_eq!(json["object"], "list");
assert_eq!(json["data"].as_array().unwrap().len(), 2);
assert_eq!(json["data"][0]["object"], "embedding");
assert_eq!(json["data"][0]["index"], 0);
assert_eq!(json["data"][1]["index"], 1);
assert_eq!(json["usage"]["prompt_tokens"], 10);
assert_eq!(json["usage"]["total_tokens"], 10);
}
#[test]
fn test_chat_request_with_tools_deserialize() {
let json = r#"{
"model": "test",
"messages": [{"role": "user", "content": "What's the weather?"}],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get weather",
"parameters": {"type": "object", "properties": {"city": {"type": "string"}}}
}
}
],
"tool_choice": "auto"
}"#;
let req: ChatCompletionRequest = serde_json::from_str(json).unwrap();
assert!(req.tools.is_some());
let tools = req.tools.unwrap();
assert_eq!(tools.len(), 1);
assert_eq!(tools[0].function.name, "get_weather");
assert!(req.tool_choice.is_some());
}
#[test]
fn test_overflow_policy_deserialize_each_variant() {
for (raw, expected) in [
("\"reject\"", OverflowPolicy::Reject),
("\"truncate_left\"", OverflowPolicy::TruncateLeft),
("\"summarize\"", OverflowPolicy::Summarize),
] {
let p: OverflowPolicy = serde_json::from_str(raw).unwrap();
assert_eq!(p, expected);
}
}
#[test]
fn test_overflow_policy_default_is_summarize() {
assert_eq!(OverflowPolicy::default(), OverflowPolicy::Summarize);
}
#[test]
fn test_message_content_text_string() {
let json = r#"{"role":"user","content":"Hello"}"#;
let msg: ChatMessage = serde_json::from_str(json).unwrap();
assert_eq!(msg.content.as_ref().unwrap().text(), "Hello");
assert!(!msg.content.as_ref().unwrap().has_images());
assert!(msg.content.as_ref().unwrap().image_urls().is_empty());
}
#[test]
fn test_message_content_null() {
let json = r#"{"role":"assistant","content":null}"#;
let msg: ChatMessage = serde_json::from_str(json).unwrap();
assert!(msg.content.is_none());
}
#[test]
fn test_message_content_vision_array() {
let json = r#"{
"role": "user",
"content": [
{"type": "text", "text": "What's in this image?"},
{"type": "image_url", "image_url": {"url": "data:image/png;base64,abc123"}}
]
}"#;
let msg: ChatMessage = serde_json::from_str(json).unwrap();
let content = msg.content.as_ref().unwrap();
assert_eq!(content.text(), "What's in this image?");
assert!(content.has_images());
let urls = content.image_urls();
assert_eq!(urls.len(), 1);
assert_eq!(urls[0], "data:image/png;base64,abc123");
}
#[test]
fn test_message_content_multiple_images() {
let json = r#"{
"role": "user",
"content": [
{"type": "text", "text": "Compare these:"},
{"type": "image_url", "image_url": {"url": "data:image/png;base64,img1"}},
{"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,img2"}}
]
}"#;
let msg: ChatMessage = serde_json::from_str(json).unwrap();
let content = msg.content.as_ref().unwrap();
assert_eq!(content.text(), "Compare these:");
let urls = content.image_urls();
assert_eq!(urls.len(), 2);
assert_eq!(urls[0], "data:image/png;base64,img1");
assert_eq!(urls[1], "data:image/jpeg;base64,img2");
}
#[test]
fn test_message_content_image_url_with_detail() {
let json = r#"{
"role": "user",
"content": [
{"type": "image_url", "image_url": {"url": "file:///tmp/test.png", "detail": "high"}}
]
}"#;
let msg: ChatMessage = serde_json::from_str(json).unwrap();
let content = msg.content.as_ref().unwrap();
assert!(content.has_images());
assert_eq!(content.image_urls()[0], "file:///tmp/test.png");
}
#[test]
fn test_message_content_text_only_array() {
let json = r#"{
"role": "user",
"content": [
{"type": "text", "text": "First part"},
{"type": "text", "text": " second part"}
]
}"#;
let msg: ChatMessage = serde_json::from_str(json).unwrap();
let content = msg.content.as_ref().unwrap();
assert_eq!(content.text(), "First part second part");
assert!(!content.has_images());
}
#[test]
fn test_message_content_as_text_opt() {
let content = MessageContent::Text("hello".to_string());
assert_eq!(content.as_text_opt(), Some("hello".to_string()));
let content = MessageContent::Text("".to_string());
assert_eq!(content.as_text_opt(), None);
}
#[test]
fn test_message_content_serialization_round_trip_text() {
let msg = ChatMessage {
role: "user".into(),
content: Some(MessageContent::Text("Hello".into())),
reasoning_content: None,
tool_calls: None,
tool_call_id: None,
name: None,
};
let json = serde_json::to_value(&msg).unwrap();
assert_eq!(json["content"], "Hello");
}
#[test]
fn test_message_content_serialization_round_trip_parts() {
let msg = ChatMessage {
role: "user".into(),
content: Some(MessageContent::Parts(vec![
ContentPart::Text {
text: "Look at this:".into(),
},
ContentPart::ImageUrl {
image_url: ImageUrl {
url: "data:image/png;base64,abc".into(),
detail: None,
},
},
])),
reasoning_content: None,
tool_calls: None,
tool_call_id: None,
name: None,
};
let json = serde_json::to_value(&msg).unwrap();
assert!(json["content"].is_array());
assert_eq!(json["content"][0]["type"], "text");
assert_eq!(json["content"][0]["text"], "Look at this:");
assert_eq!(json["content"][1]["type"], "image_url");
assert_eq!(
json["content"][1]["image_url"]["url"],
"data:image/png;base64,abc"
);
}
#[test]
fn test_logprobs_serialization() {
let lp = ChoiceLogprobs {
content: vec![TokenLogprob {
token: "Hello".into(),
logprob: -0.5,
bytes: Some(vec![72, 101, 108, 108, 111]),
top_logprobs: vec![TopLogprobEntry {
token: "Hi".into(),
logprob: -1.2,
bytes: Some(vec![72, 105]),
}],
}],
};
let json = serde_json::to_value(&lp).unwrap();
assert_eq!(json["content"][0]["token"], "Hello");
assert!((json["content"][0]["logprob"].as_f64().unwrap() - -0.5).abs() < 1e-6);
assert_eq!(json["content"][0]["top_logprobs"][0]["token"], "Hi");
}
#[test]
fn c3_schema_queue_full_docstring_names_scheduler_policy() {
let source = include_str!("schema.rs");
let queue_full_pos = source
.find("pub fn queue_full() -> Self")
.expect("source must contain `pub fn queue_full() -> Self`");
let preamble = &source[..queue_full_pos];
let docblock_lines: Vec<&str> = preamble
.lines()
.rev()
.take_while(|line| {
let trimmed = line.trim_start();
trimmed.starts_with("///") || trimmed.is_empty()
})
.collect();
let docblock = docblock_lines
.into_iter()
.rev()
.collect::<Vec<_>>()
.join("\n");
assert!(
docblock.contains("SchedulerPolicy"),
"ADR-040 C3: the queue_full() docstring MUST name \
`SchedulerPolicy` (per ADR-040 §6.1.9 C4 SHIPPED). \
Current doc block:\n{docblock}"
);
assert!(
docblock.contains("Decision #2"),
"ADR-040 C3: the queue_full() docstring MUST cite \
ADR-005 Decision #2 (the carve-out this scheduler \
selection sits alongside). Current doc block:\n{docblock}"
);
assert!(
docblock.contains("FifoSerial") && docblock.contains("InflightBatched"),
"ADR-040 C3 (iter-A5b MAJOR #1 fix): the queue_full() \
docstring MUST name BOTH real `SchedulerPolicy` variants \
— `FifoSerial` (default under Decision #19) and \
`InflightBatched` (the real Phase C2c+ scheduler-policy \
enum variant). The doc must NOT name a nonexistent \
`SchedulerPolicy::SlotAware` variant. Current doc \
block:\n{docblock}"
);
assert!(
docblock.contains("EngineMode::SlotAware"),
"ADR-040 C3 (iter-A5b MAJOR #1 fix): the queue_full() \
docstring MUST name `EngineMode::SlotAware` as the \
engine-mode enum variant gating the InflightBatched \
runtime (distinct from the SchedulerPolicy variant). \
Current doc block:\n{docblock}"
);
assert!(
!docblock.contains("SchedulerPolicy::SlotAware"),
"ADR-040 C3 (iter-A5b MAJOR #1 fix): the queue_full() \
docstring MUST NOT reference `SchedulerPolicy::SlotAware` \
— that variant does not exist on the SchedulerPolicy enum \
(variants are `FifoSerial` + `InflightBatched`). \
SlotAware lives on the SEPARATE `EngineMode` enum. \
Current doc block:\n{docblock}"
);
assert!(
docblock.contains("ADR-040"),
"ADR-040 C3: the queue_full() docstring MUST reference \
ADR-040 so operators searching for the scheduler-policy \
surface land on this method. Current doc block:\n{docblock}"
);
}
#[test]
fn c3_schema_slot_budget_exceeded_returns_429_with_retry_after() {
let needed = 5 * 1024 * 1024u64;
let budget = 4 * 1024 * 1024u64;
let err = ApiError::slot_budget_exceeded(needed, budget);
assert_eq!(
err.status,
StatusCode::TOO_MANY_REQUESTS,
"ADR-040 §3.5 A5: SlotBudgetExceeded MUST map to HTTP 429 \
(per Decision #19, parallel to queue_full)"
);
assert_eq!(
err.error.error_type, "server_error",
"ADR-040 §3.5 A5: error_type follows queue_full convention \
(server_error class)"
);
assert_eq!(
err.error.code.as_deref(),
Some("slot_budget_exceeded"),
"ADR-040 §3.5 A5: code MUST be `slot_budget_exceeded` \
(distinct from queue_full so observability + alerting \
can differentiate the two 429 emitters)"
);
assert_eq!(
err.retry_after_seconds,
Some(1),
"ADR-040 §3.5 A5: Retry-After: 1 mirrors queue_full \
(Decision #19 wire-level contract preserved)"
);
assert!(
err.error.message.contains(&needed.to_string()),
"ADR-040 §3.5 A5: message MUST embed needed_bytes verbatim. \
Got: {}",
err.error.message
);
assert!(
err.error.message.contains(&budget.to_string()),
"ADR-040 §3.5 A5: message MUST embed budget_bytes verbatim. \
Got: {}",
err.error.message
);
assert!(
err.error.message.contains("max_tokens"),
"ADR-040 §3.5 A5: message MUST cite max_tokens as a \
remediation lever so the operator knows what to change. \
Got: {}",
err.error.message
);
assert!(
err.error.message.contains("prompt"),
"ADR-040 §3.5 A5: message MUST cite the prompt-shortening \
remediation. Got: {}",
err.error.message
);
assert!(
err.error.message.contains("ADR-040"),
"ADR-040 §3.5 A5: message MUST cite ADR-040 §3.5 so \
operators can find the canonical documentation. Got: {}",
err.error.message
);
let response = err.into_response();
assert_eq!(
response.status(),
StatusCode::TOO_MANY_REQUESTS,
"ADR-040 §3.5 A5: rendered Response status MUST be 429"
);
assert_eq!(
response
.headers()
.get("retry-after")
.and_then(|v| v.to_str().ok()),
Some("1"),
"ADR-040 §3.5 A5: rendered Response MUST carry \
Retry-After: 1 (Decision #19)"
);
}
#[test]
fn c3_schema_capability_unsupported_maps_to_501() {
let err = ApiError::capability_unsupported(
"fork_seq cross-slot copy (Qwen35 HybridKvCache; deferred to Phase A2c)",
);
assert_eq!(
err.status,
StatusCode::NOT_IMPLEMENTED,
"ADR-040 C3: MultiSeqError::CapabilityUnsupported MUST \
map to HTTP 501 Not Implemented (distinct from \
SlotOom→429 and SlotOutOfRange→500)"
);
assert_eq!(
err.error.error_type, "server_error",
"ADR-040 C3: error_type follows the iter-215 Wedge-2 \
`not_implemented` convention (server_error class)"
);
assert_eq!(
err.error.code.as_deref(),
Some("capability_unsupported"),
"ADR-040 C3: the `code` field MUST be \
`capability_unsupported` so observability + alerting \
can differentiate from other 501 emitters"
);
assert!(
err.error.message.contains("fork_seq cross-slot copy"),
"ADR-040 C3: the rendered message MUST name the \
unsupported capability so operators know which trait \
method is the bottleneck. Got: {}",
err.error.message
);
assert!(
err.error.message.contains("ADR-040"),
"ADR-040 C3: the rendered message MUST cite ADR-040 \
§6 Phase C C3 so the operator can find the canonical \
documentation. Got: {}",
err.error.message
);
let response = err.into_response();
assert_eq!(
response.status(),
StatusCode::NOT_IMPLEMENTED,
"ADR-040 C3: the rendered HTTP Response status MUST be \
501 Not Implemented (RFC 7231 §6.6.2 — caller's request \
is well-formed; the server's capability surface is the \
bottleneck)"
);
assert!(
response.headers().get("retry-after").is_none(),
"ADR-040 C3: 501 Not Implemented is NOT transient — the \
unsupported capability requires a future iter to ship; \
no Retry-After should be emitted (unlike 429 queue_full)"
);
}
}