Skip to main content

ferrum_server/
axum_server.rs

1//! Axum-based HTTP server implementation for Ferrum
2//!
3//! This module provides a concrete implementation of the HttpServer trait
4//! using the Axum web framework, with OpenAI-shaped endpoint compatibility.
5
6use crate::{
7    chat_template::{
8        render_chat_prompt_with_model_template_options_and_compatibility,
9        render_chat_prompt_with_tools_and_model_template_compatibility, ChatTemplateOptions,
10        ModelChatTemplate, ModelReasoningProtocol, ReasoningEffort,
11    },
12    model_registry::{LoraAdapterModel, ServedModelKind, ServedModelRegistry},
13    openai::*,
14    traits::HttpServer,
15    types::*,
16};
17use async_trait::async_trait;
18use axum::{
19    extract::{multipart::MultipartRejection, rejection::JsonRejection, State},
20    http::{HeaderMap, StatusCode as AxumStatusCode},
21    response::{sse::Event, IntoResponse, Response, Sse},
22    routing::{get, post},
23    Json, Router,
24};
25use ferrum_bench_core::{
26    BenchmarkRequestCorrelation, BENCHMARK_CELL_ID_HEADER, BENCHMARK_PHASE_HEADER,
27    BENCHMARK_REPEAT_INDEX_HEADER, BENCHMARK_REQUEST_INDEX_HEADER, BENCHMARK_RUN_ID_HEADER,
28};
29use ferrum_interfaces::engine::{EmbedEngine, LlmInferenceEngine, TranscribeEngine, TtsEngine};
30use ferrum_types::{
31    has_unclosed_model_reasoning_block, model_reasoning_markers,
32    parse_harmony_response_for_finish_reason, parse_model_reasoning_response,
33    should_defer_model_reasoning_stream_delta, EngineMetrics, EngineStatus, FerrumConfigBuilder,
34    FerrumError as Error, FerrumProfileEvent, FinishReason, InferenceExecutionEvidence,
35    InferenceRequest, InferenceResponse, ModelId, ModelOutputProtocol, ParsedReasoningResponse,
36    Priority, ProcessMemoryObservation, ProcessMemorySample, ProcessMemorySampler,
37    ProfileEntrypoint, ProfileError, ProfileEventKind, ProfileStatus, ReplayReference, RequestId,
38    ResolvedFerrumConfig, ResourceAction, ResourceTraceEvent, ResponseCompletionBoundary,
39    RuntimeConfigSnapshot, SamplingParams, StructuredOutputStart, TokenId, TokenUsage,
40    DEFAULT_CHAT_REPETITION_PENALTY, DEFAULT_MAX_TOKENS_METADATA_KEY,
41    OBSERVABILITY_PROFILE_SCHEMA_VERSION, THINK_END_TAG, THINK_START_TAG,
42};
43use sha2::{Digest, Sha256};
44use std::{
45    collections::{BTreeMap, HashMap},
46    error::Error as StdError,
47    fs,
48    path::{Path, PathBuf},
49    sync::{
50        atomic::{AtomicBool, Ordering},
51        Arc, Mutex, OnceLock,
52    },
53    time::Instant,
54};
55use tokio::sync::{mpsc, Notify};
56use tokio_stream::StreamExt;
57use tower::ServiceBuilder;
58use tower_http::{cors::CorsLayer, trace::TraceLayer};
59use tracing::{debug, error, info, span, warn, Level};
60use uuid::Uuid;
61
62mod responses;
63
64const DEFAULT_SAMPLING_TEMPERATURE: f32 = 0.0;
65const DEFAULT_SAMPLING_TOP_P: f32 = 1.0;
66const DEFAULT_COMPLETION_MAX_TOKENS: u32 = 4096;
67const INITIAL_FORBIDDEN_TOKEN_TEXTS_METADATA_KEY: &str = "ferrum_initial_forbidden_token_texts";
68const DEFAULT_GUIDED_TOOL_ARGUMENT_STRING_MAX_LENGTH: u64 = 128;
69const MAX_CACHED_JSON_SCHEMA_VALIDATORS: usize = 64;
70const INITIAL_STRUCTURED_CALL_FORBIDDEN_TOKEN_TEXTS: &[&str] =
71    &["<|im_end|>", "<|endoftext|>", "<|eot_id|>", "</s>"];
72const FERRUM_SESSION_HEADER: &str = "x-ferrum-session";
73static JSON_SCHEMA_VALIDATOR_CACHE: OnceLock<Mutex<HashMap<String, Arc<jsonschema::Validator>>>> =
74    OnceLock::new();
75
76/// Product defaults used when an OpenAI chat request omits sampling fields.
77/// The engine composition root consumes the same typed value so its resolved
78/// startup plan cannot describe different defaults from the HTTP endpoint.
79pub fn default_chat_sampling_params() -> SamplingParams {
80    SamplingParams {
81        max_tokens: DEFAULT_COMPLETION_MAX_TOKENS as usize,
82        temperature: DEFAULT_SAMPLING_TEMPERATURE,
83        top_p: DEFAULT_SAMPLING_TOP_P,
84        repetition_penalty: DEFAULT_CHAT_REPETITION_PENALTY,
85        ..SamplingParams::default()
86    }
87}
88
89#[derive(Debug, Clone)]
90struct CachePolicy {
91    prefix_cache_enabled: bool,
92    session_cache_mode: String,
93    session_cache_max_entries: usize,
94    session_cache_max_tokens: usize,
95}
96
97impl CachePolicy {
98    fn current() -> Self {
99        Self {
100            prefix_cache_enabled: env_bool("FERRUM_PREFIX_CACHE_PRODUCT")
101                .or_else(|| env_bool("FERRUM_PREFIX_CACHE_REQUESTED"))
102                .or_else(|| env_bool("FERRUM_PREFIX_CACHE"))
103                .unwrap_or(false),
104            session_cache_mode: std::env::var("FERRUM_SESSION_CACHE")
105                .unwrap_or_else(|_| "off".to_string())
106                .to_ascii_lowercase(),
107            session_cache_max_entries: env_usize("FERRUM_SESSION_CACHE_MAX_ENTRIES").unwrap_or(128),
108            session_cache_max_tokens: env_usize("FERRUM_SESSION_CACHE_MAX_TOKENS").unwrap_or(4096),
109        }
110    }
111
112    fn session_memory_enabled(&self) -> bool {
113        self.session_cache_mode == "memory"
114    }
115}
116
117fn env_bool(key: &str) -> Option<bool> {
118    match std::env::var(key).ok()?.to_ascii_lowercase().as_str() {
119        "1" | "true" | "yes" | "on" => Some(true),
120        "0" | "false" | "no" | "off" => Some(false),
121        _ => None,
122    }
123}
124
125fn env_usize(key: &str) -> Option<usize> {
126    std::env::var(key).ok()?.parse().ok()
127}
128
129/// Shared Prometheus recorder handle for rendering metrics.
130static PROM_HANDLE: std::sync::OnceLock<metrics_exporter_prometheus::PrometheusHandle> =
131    std::sync::OnceLock::new();
132
133/// Initialize the Prometheus metrics recorder.
134///
135/// Must be called once before any `metrics::counter!()` / `histogram!()` calls.
136/// Safe to call multiple times — subsequent calls are no-ops.
137pub fn init_prometheus_recorder() {
138    PROM_HANDLE.get_or_init(|| {
139        let builder = metrics_exporter_prometheus::PrometheusBuilder::new();
140        let handle = builder
141            .install_recorder()
142            .expect("Failed to install Prometheus recorder");
143        info!("Prometheus metrics recorder installed");
144        handle
145    });
146}
147
148/// Axum-based server implementation.
149///
150/// The server is built around [`AppState`], which holds an optional
151/// engine per modality. Handlers fault to 503 when the modality they
152/// need isn't loaded, instead of running stub error logic.
153pub struct AxumServer {
154    state: AppState,
155    config: ServerConfig,
156    lifecycle: Arc<AxumServerLifecycle>,
157}
158
159#[derive(Default)]
160struct AxumServerLifecycle {
161    shutdown_requested: AtomicBool,
162    running: AtomicBool,
163    engines_stopped: AtomicBool,
164    shutdown_notify: Notify,
165    stopped_notify: Notify,
166    stop_lock: tokio::sync::Mutex<()>,
167}
168
169impl AxumServerLifecycle {
170    fn request_shutdown(&self) {
171        self.shutdown_requested.store(true, Ordering::Release);
172        self.shutdown_notify.notify_waiters();
173    }
174
175    async fn wait_for_shutdown(&self) {
176        while !self.shutdown_requested.load(Ordering::Acquire) {
177            self.shutdown_notify.notified().await;
178        }
179    }
180
181    async fn wait_until_stopped(&self) {
182        while self.running.load(Ordering::Acquire) {
183            self.stopped_notify.notified().await;
184        }
185    }
186}
187
188struct AxumServerRunGuard {
189    lifecycle: Arc<AxumServerLifecycle>,
190}
191
192impl Drop for AxumServerRunGuard {
193    fn drop(&mut self) {
194        self.lifecycle.running.store(false, Ordering::Release);
195        self.lifecycle.stopped_notify.notify_waiters();
196    }
197}
198
199fn single_model_registry(engine_model_id: ModelId, kind: ServedModelKind) -> ServedModelRegistry {
200    let public_name = engine_model_id.to_string();
201    ServedModelRegistry::try_new(engine_model_id, kind, vec![public_name], vec![])
202        .expect("engine config must contain a valid model id")
203}
204
205impl AxumServer {
206    /// Create a server with a fully populated AppState.
207    pub fn from_state(state: AppState) -> Self {
208        Self {
209            state,
210            config: ServerConfig::default(),
211            lifecycle: Arc::new(AxumServerLifecycle::default()),
212        }
213    }
214
215    /// Convenience constructor for an LLM-only server (chat / completions).
216    pub fn from_llm(engine: Arc<dyn LlmInferenceEngine + Send + Sync>) -> Self {
217        Self::from_state(AppState::default().with_llm(engine))
218    }
219
220    /// Convenience constructor for an embedding-only server (`/v1/embeddings`).
221    pub fn from_embed(engine: Arc<dyn EmbedEngine + Send + Sync>) -> Self {
222        Self::from_state(AppState::default().with_embed(engine))
223    }
224
225    /// Convenience constructor for a transcription-only server
226    /// (`/v1/audio/transcriptions`).
227    pub fn from_transcribe(engine: Arc<dyn TranscribeEngine + Send + Sync>) -> Self {
228        Self::from_state(AppState::default().with_transcribe(engine))
229    }
230
231    /// Convenience constructor for a TTS-only server (`/v1/audio/speech`).
232    pub fn from_tts(engine: Arc<dyn TtsEngine + Send + Sync>) -> Self {
233        Self::from_state(AppState::default().with_tts(engine))
234    }
235
236    /// Attach the startup auto-configuration decision trace exposed by
237    /// `/health`. Constructors keep this optional so tests and non-LLM
238    /// deployments can use the server without a model-specific resolver.
239    pub fn with_auto_config(mut self, auto_config: ResolvedFerrumConfig) -> Self {
240        self.state = self.state.with_auto_config(auto_config);
241        self
242    }
243
244    /// Attach the loaded model's prompt template, if available. This keeps
245    /// OpenAI request aliases from driving prompt-family selection.
246    pub fn with_prompt_template(mut self, prompt_template: Option<ModelChatTemplate>) -> Self {
247        self.state = self.state.with_prompt_template(prompt_template);
248        self
249    }
250
251    /// Set the server-wide reasoning default for requests that omit
252    /// `chat_template_kwargs.enable_thinking`. Per-request values still win.
253    pub fn with_default_enable_thinking(mut self, enable_thinking: Option<bool>) -> Self {
254        self.state = self.state.with_default_enable_thinking(enable_thinking);
255        self
256    }
257
258    /// Control the compatibility retry for model templates that reject
259    /// non-leading system messages. It is enabled by default.
260    pub fn with_interleaved_system_coalescing(mut self, enabled: bool) -> Self {
261        self.state = self.state.with_interleaved_system_coalescing(enabled);
262        self
263    }
264
265    /// Install the public OpenAI model namespace used for request routing and
266    /// `/v1/models`. The registry keeps public aliases separate from the
267    /// engine's internal model id.
268    pub fn with_served_model_registry(mut self, registry: ServedModelRegistry) -> Self {
269        self.state = self.state.with_served_model_registry(registry);
270        self
271    }
272
273    /// Attach startup-loaded LoRA adapter model ids.
274    pub fn with_lora_adapters(
275        mut self,
276        base_model_id: impl Into<String>,
277        adapters: Vec<LoraAdapterModel>,
278    ) -> ferrum_types::Result<Self> {
279        let base_model_id = base_model_id.into();
280        let registry = if self.state.served_model_registry.is_empty() {
281            ServedModelRegistry::try_new(
282                base_model_id.clone(),
283                ServedModelKind::Llm,
284                vec![base_model_id],
285                adapters,
286            )
287        } else {
288            self.state
289                .served_model_registry
290                .try_with_lora_adapters(&base_model_id, adapters)
291        }
292        .map_err(|error| Error::config(error.to_string()))?;
293        self.state = self.state.with_served_model_registry(registry);
294        Ok(self)
295    }
296
297    async fn shutdown_loaded_engines(&self) -> ferrum_types::Result<()> {
298        let mut first_error = None;
299        if let Some(engine) = &self.state.llm {
300            if let Err(error) = engine.shutdown().await {
301                first_error = Some(error);
302            }
303        }
304        if let Some(engine) = &self.state.embed {
305            if let Err(error) = engine.shutdown().await {
306                if first_error.is_none() {
307                    first_error = Some(error);
308                }
309            }
310        }
311        if let Some(engine) = &self.state.transcribe {
312            if let Err(error) = engine.shutdown().await {
313                if first_error.is_none() {
314                    first_error = Some(error);
315                }
316            }
317        }
318        if let Some(engine) = &self.state.tts {
319            if let Err(error) = engine.shutdown().await {
320                if first_error.is_none() {
321                    first_error = Some(error);
322                }
323            }
324        }
325        first_error.map_or(Ok(()), Err)
326    }
327
328    /// Build the router with all routes
329    #[allow(dead_code)]
330    fn build_router(&self) -> Router {
331        self.build_router_with_state(self.state.clone())
332    }
333
334    fn build_router_with_state(&self, app_state: AppState) -> Router {
335        Router::new()
336            // OpenAI API routes
337            .route("/v1/chat/completions", post(chat_completions_handler))
338            .route("/v1/responses", post(responses::responses_handler))
339            .route("/v1/completions", post(completions_handler))
340            .route("/v1/embeddings", post(embeddings_handler))
341            .route("/v1/audio/transcriptions", post(transcriptions_handler))
342            .route("/v1/audio/speech", post(speech_handler))
343            .route("/v1/models", get(models_handler))
344            // Health & observability
345            .route("/health", get(health_handler))
346            .route("/metrics", get(metrics_handler))
347            .route("/", get(root_handler))
348            // Apply middleware
349            .layer(
350                ServiceBuilder::new()
351                    .layer(TraceLayer::new_for_http())
352                    .layer(CorsLayer::permissive()), // For MVP, allow all origins
353            )
354            .with_state(app_state)
355    }
356}
357
358/// Application state shared across handlers — one optional engine per
359/// modality. Handlers reach into the field they need and 503 when it's
360/// not loaded.
361#[derive(Clone, Default)]
362pub struct AppState {
363    pub llm: Option<Arc<dyn LlmInferenceEngine + Send + Sync>>,
364    pub embed: Option<Arc<dyn EmbedEngine + Send + Sync>>,
365    pub transcribe: Option<Arc<dyn TranscribeEngine + Send + Sync>>,
366    pub tts: Option<Arc<dyn TtsEngine + Send + Sync>>,
367    pub auto_config: Option<ResolvedFerrumConfig>,
368    pub prompt_template: Option<Arc<ModelChatTemplate>>,
369    pub default_enable_thinking: Option<bool>,
370    interleaved_system_coalescing: Option<bool>,
371    pub served_model_registry: Arc<ServedModelRegistry>,
372    pub request_dump_dir: Option<Arc<PathBuf>>,
373    pub profile_jsonl: Option<Arc<PathBuf>>,
374    pub profile_detail: ferrum_types::ObservabilityProfileDetail,
375    pub memory_profile_jsonl: Option<Arc<PathBuf>>,
376    pub first_request_memory_recorded: Arc<AtomicBool>,
377    cache: Arc<CacheRuntimeState>,
378}
379
380impl AppState {
381    pub fn with_llm(mut self, engine: Arc<dyn LlmInferenceEngine + Send + Sync>) -> Self {
382        if self.served_model_registry.is_empty() {
383            self.served_model_registry = Arc::new(single_model_registry(
384                engine.config().model.model_id.clone(),
385                ServedModelKind::Llm,
386            ));
387        }
388        self.llm = Some(engine);
389        self
390    }
391    pub fn with_embed(mut self, engine: Arc<dyn EmbedEngine + Send + Sync>) -> Self {
392        if self.served_model_registry.is_empty() {
393            self.served_model_registry = Arc::new(single_model_registry(
394                engine.config().model.model_id.clone(),
395                ServedModelKind::Embedding,
396            ));
397        }
398        self.embed = Some(engine);
399        self
400    }
401    pub fn with_transcribe(mut self, engine: Arc<dyn TranscribeEngine + Send + Sync>) -> Self {
402        if self.served_model_registry.is_empty() {
403            self.served_model_registry = Arc::new(single_model_registry(
404                engine.config().model.model_id.clone(),
405                ServedModelKind::Transcription,
406            ));
407        }
408        self.transcribe = Some(engine);
409        self
410    }
411    pub fn with_tts(mut self, engine: Arc<dyn TtsEngine + Send + Sync>) -> Self {
412        if self.served_model_registry.is_empty() {
413            self.served_model_registry = Arc::new(single_model_registry(
414                engine.config().model.model_id.clone(),
415                ServedModelKind::Speech,
416            ));
417        }
418        self.tts = Some(engine);
419        self
420    }
421
422    pub fn with_auto_config(mut self, auto_config: ResolvedFerrumConfig) -> Self {
423        self.auto_config = Some(auto_config);
424        self
425    }
426
427    pub fn with_prompt_template(mut self, prompt_template: Option<ModelChatTemplate>) -> Self {
428        self.prompt_template = prompt_template.map(Arc::new);
429        self
430    }
431
432    pub fn with_default_enable_thinking(mut self, enable_thinking: Option<bool>) -> Self {
433        self.default_enable_thinking = enable_thinking;
434        self
435    }
436
437    pub fn with_interleaved_system_coalescing(mut self, enabled: bool) -> Self {
438        self.interleaved_system_coalescing = Some(enabled);
439        self
440    }
441
442    pub fn with_served_model_registry(mut self, registry: ServedModelRegistry) -> Self {
443        self.served_model_registry = Arc::new(registry);
444        self
445    }
446
447    pub fn with_request_dump_dir(mut self, request_dump_dir: Option<PathBuf>) -> Self {
448        self.request_dump_dir = request_dump_dir.map(Arc::new);
449        self
450    }
451
452    pub fn with_profile_jsonl(mut self, profile_jsonl: Option<PathBuf>) -> Self {
453        self.profile_jsonl = profile_jsonl.map(Arc::new);
454        self
455    }
456
457    pub fn with_profile_detail(
458        mut self,
459        profile_detail: ferrum_types::ObservabilityProfileDetail,
460    ) -> Self {
461        self.profile_detail = profile_detail;
462        self
463    }
464
465    pub fn with_memory_profile_jsonl(mut self, memory_profile_jsonl: Option<PathBuf>) -> Self {
466        self.memory_profile_jsonl = memory_profile_jsonl.map(Arc::new);
467        self
468    }
469
470    /// Async aggregated status across whichever modality is loaded.
471    /// In single-modality deployments (current CLI), exactly one is Some.
472    async fn status(&self) -> EngineStatus {
473        if let Some(e) = &self.llm {
474            return e.status().await;
475        }
476        if let Some(e) = &self.embed {
477            return e.status().await;
478        }
479        if let Some(e) = &self.transcribe {
480            return e.status().await;
481        }
482        if let Some(e) = &self.tts {
483            return e.status().await;
484        }
485        EngineStatus {
486            is_ready: false,
487            loaded_models: vec![],
488            active_requests: 0,
489            queued_requests: 0,
490            memory_usage: ferrum_types::MemoryUsage {
491                total_bytes: 0,
492                used_bytes: 0,
493                free_bytes: 0,
494                gpu_memory_bytes: None,
495                cpu_memory_bytes: None,
496                cache_memory_bytes: 0,
497                utilization_percent: 0.0,
498            },
499            uptime_seconds: 0,
500            last_heartbeat: chrono::Utc::now(),
501            version: env!("CARGO_PKG_VERSION").to_string(),
502        }
503    }
504
505    fn metrics(&self) -> EngineMetrics {
506        if let Some(e) = &self.llm {
507            return e.metrics();
508        }
509        if let Some(e) = &self.embed {
510            return e.metrics();
511        }
512        if let Some(e) = &self.transcribe {
513            return e.metrics();
514        }
515        if let Some(e) = &self.tts {
516            return e.metrics();
517        }
518        EngineMetrics {
519            total_requests: 0,
520            successful_requests: 0,
521            failed_requests: 0,
522            avg_request_latency_ms: 0.0,
523            p95_request_latency_ms: 0.0,
524            p99_request_latency_ms: 0.0,
525            throughput_rps: 0.0,
526            tokens_per_second: 0.0,
527            queue_metrics: Default::default(),
528            resource_utilization: Default::default(),
529            error_stats: Default::default(),
530            performance_breakdown: Default::default(),
531        }
532    }
533}
534
535#[derive(Default)]
536struct CacheRuntimeState {
537    stats: Mutex<CacheStats>,
538    prefix_prompts: Mutex<HashMap<String, usize>>,
539    sessions: Mutex<HashMap<String, Vec<ChatMessage>>>,
540}
541
542#[derive(Debug, Clone, Default)]
543struct CacheStats {
544    prefix_hits: u64,
545    prefix_misses: u64,
546    prefix_evictions: u64,
547    prefix_saved_prefill_tokens: u64,
548    prefix_entries: u64,
549    prefix_bytes: u64,
550    session_hits: u64,
551    session_misses: u64,
552    session_evictions: u64,
553    session_entries: u64,
554    session_tokens: u64,
555}
556
557#[derive(Clone)]
558struct SessionContext {
559    id: String,
560    prior_messages: Vec<ChatMessage>,
561    incoming_messages: Vec<ChatMessage>,
562}
563
564impl CacheRuntimeState {
565    fn record_prefix_prompt(&self, prompt: &str, policy: &CachePolicy) {
566        if !policy.prefix_cache_enabled {
567            return;
568        }
569
570        let prompt_tokens = approx_tokens(prompt);
571        let mut prompts = self.prefix_prompts.lock().expect("prefix cache lock");
572        let saved_tokens = prompts
573            .keys()
574            .map(|seen| approx_tokens_for_chars(longest_common_prefix_chars(seen, prompt)))
575            .max()
576            .unwrap_or(0);
577
578        let mut stats = self.stats.lock().expect("cache stats lock");
579        if saved_tokens > 0 {
580            stats.prefix_hits += 1;
581            stats.prefix_saved_prefill_tokens += saved_tokens as u64;
582        } else {
583            stats.prefix_misses += 1;
584        }
585
586        let max_entries = policy.session_cache_max_entries.max(1);
587        if !prompts.contains_key(prompt) && prompts.len() >= max_entries {
588            if let Some(key) = prompts.keys().next().cloned() {
589                prompts.remove(&key);
590                stats.prefix_evictions += 1;
591            }
592        }
593        prompts.insert(prompt.to_string(), prompt_tokens);
594        stats.prefix_entries = prompts.len() as u64;
595        stats.prefix_bytes = prompts.keys().map(|key| key.len() as u64).sum();
596    }
597
598    fn prepare_session_request(
599        &self,
600        request: &mut ChatCompletionsRequest,
601        headers: &HeaderMap,
602        policy: &CachePolicy,
603    ) -> Option<SessionContext> {
604        let session_id = request_session_id(headers, request)?;
605        if !policy.session_memory_enabled() {
606            return None;
607        }
608
609        let incoming_messages = request.messages.clone();
610        let prior_messages = {
611            let sessions = self.sessions.lock().expect("session cache lock");
612            sessions.get(&session_id).cloned().unwrap_or_default()
613        };
614        {
615            let mut stats = self.stats.lock().expect("cache stats lock");
616            if prior_messages.is_empty() {
617                stats.session_misses += 1;
618            } else {
619                stats.session_hits += 1;
620                let mut merged = prior_messages.clone();
621                merged.extend(request.messages.clone());
622                request.messages = merged;
623            }
624        }
625
626        Some(SessionContext {
627            id: session_id,
628            prior_messages,
629            incoming_messages,
630        })
631    }
632
633    fn update_session(
634        &self,
635        context: Option<SessionContext>,
636        assistant_message: ChatMessage,
637        policy: &CachePolicy,
638    ) {
639        let Some(context) = context else {
640            return;
641        };
642        if !policy.session_memory_enabled() {
643            return;
644        }
645
646        let mut history = context.prior_messages;
647        history.extend(context.incoming_messages);
648        history.push(assistant_message);
649        trim_messages_to_token_budget(&mut history, policy.session_cache_max_tokens);
650
651        let mut sessions = self.sessions.lock().expect("session cache lock");
652        if !sessions.contains_key(&context.id)
653            && sessions.len() >= policy.session_cache_max_entries.max(1)
654        {
655            if let Some(evict_key) = sessions.keys().next().cloned() {
656                sessions.remove(&evict_key);
657                self.stats
658                    .lock()
659                    .expect("cache stats lock")
660                    .session_evictions += 1;
661            }
662        }
663        sessions.insert(context.id, history);
664
665        let entries = sessions.len() as u64;
666        let tokens = sessions
667            .values()
668            .map(|messages| {
669                messages
670                    .iter()
671                    .map(|msg| approx_tokens(&msg.content))
672                    .sum::<usize>()
673            })
674            .sum::<usize>() as u64;
675        let mut stats = self.stats.lock().expect("cache stats lock");
676        stats.session_entries = entries;
677        stats.session_tokens = tokens;
678    }
679
680    fn stats(&self) -> CacheStats {
681        let mut stats = self.stats.lock().expect("cache stats lock").clone();
682        stats.prefix_entries = self.prefix_prompts.lock().expect("prefix cache lock").len() as u64;
683        let sessions = self.sessions.lock().expect("session cache lock");
684        stats.session_entries = sessions.len() as u64;
685        stats.session_tokens = sessions
686            .values()
687            .map(|messages| {
688                messages
689                    .iter()
690                    .map(|msg| approx_tokens(&msg.content))
691                    .sum::<usize>()
692            })
693            .sum::<usize>() as u64;
694        stats
695    }
696
697    fn health_json(
698        &self,
699        policy: &CachePolicy,
700        engine_prefix_cache: Option<&serde_json::Value>,
701    ) -> serde_json::Value {
702        let stats = self.stats();
703        let prefix_hits = engine_u64(engine_prefix_cache, "hits").unwrap_or(stats.prefix_hits);
704        let prefix_misses =
705            engine_u64(engine_prefix_cache, "misses").unwrap_or(stats.prefix_misses);
706        let prefix_evictions =
707            engine_u64(engine_prefix_cache, "evictions").unwrap_or(stats.prefix_evictions);
708        let prefix_saved = engine_u64(engine_prefix_cache, "saved_prefill_tokens")
709            .unwrap_or(stats.prefix_saved_prefill_tokens);
710        let prefix_entries =
711            engine_u64(engine_prefix_cache, "entries").unwrap_or(stats.prefix_entries);
712        let prefix_bytes = engine_u64(engine_prefix_cache, "bytes").unwrap_or(stats.prefix_bytes);
713        let mut prefix_cache = serde_json::json!({
714            "enabled": engine_bool(engine_prefix_cache, "enabled").unwrap_or(policy.prefix_cache_enabled),
715            "position": engine_str(engine_prefix_cache, "position").unwrap_or("product-observability"),
716            "source": engine_str(engine_prefix_cache, "source").unwrap_or("server-prompt-lcp-observability"),
717            "entries": prefix_entries,
718            "hits": prefix_hits,
719            "misses": prefix_misses,
720            "evictions": prefix_evictions,
721            "saved_prefill_tokens": prefix_saved,
722            "bytes": prefix_bytes,
723            "block_size": engine_u64(engine_prefix_cache, "block_size"),
724            "kv_dtype": engine_str(engine_prefix_cache, "kv_dtype"),
725        });
726        if let (Some(engine), Some(prefix)) = (
727            engine_prefix_cache.and_then(|value| value.as_object()),
728            prefix_cache.as_object_mut(),
729        ) {
730            for (key, value) in engine {
731                prefix.entry(key.clone()).or_insert_with(|| value.clone());
732            }
733        }
734        serde_json::json!({
735            "prefix_cache": prefix_cache,
736            "session_cache": {
737                "mode": policy.session_cache_mode,
738                "entries": stats.session_entries,
739                "hits": stats.session_hits,
740                "misses": stats.session_misses,
741                "evictions": stats.session_evictions,
742                "tokens": stats.session_tokens,
743                "max_entries": policy.session_cache_max_entries,
744                "max_tokens": policy.session_cache_max_tokens,
745            }
746        })
747    }
748
749    fn prometheus_metrics(&self, engine_prefix_cache: Option<&serde_json::Value>) -> String {
750        let stats = self.stats();
751        let prefix_hits = engine_u64(engine_prefix_cache, "hits").unwrap_or(stats.prefix_hits);
752        let prefix_misses =
753            engine_u64(engine_prefix_cache, "misses").unwrap_or(stats.prefix_misses);
754        let prefix_evictions =
755            engine_u64(engine_prefix_cache, "evictions").unwrap_or(stats.prefix_evictions);
756        let prefix_saved = engine_u64(engine_prefix_cache, "saved_prefill_tokens")
757            .unwrap_or(stats.prefix_saved_prefill_tokens);
758        let prefix_entries =
759            engine_u64(engine_prefix_cache, "entries").unwrap_or(stats.prefix_entries);
760        let prefix_bytes = engine_u64(engine_prefix_cache, "bytes").unwrap_or(stats.prefix_bytes);
761        format!(
762            concat!(
763                "ferrum_prefix_cache_hits_total {}\n",
764                "ferrum_prefix_cache_misses_total {}\n",
765                "ferrum_prefix_cache_evictions_total {}\n",
766                "ferrum_prefix_cache_saved_prefill_tokens_total {}\n",
767                "ferrum_prefix_cache_entries {}\n",
768                "ferrum_prefix_cache_bytes {}\n",
769                "ferrum_session_cache_hits_total {}\n",
770                "ferrum_session_cache_misses_total {}\n",
771                "ferrum_session_cache_evictions_total {}\n",
772                "ferrum_session_cache_entries {}\n",
773                "ferrum_session_cache_tokens {}\n"
774            ),
775            prefix_hits,
776            prefix_misses,
777            prefix_evictions,
778            prefix_saved,
779            prefix_entries,
780            prefix_bytes,
781            stats.session_hits,
782            stats.session_misses,
783            stats.session_evictions,
784            stats.session_entries,
785            stats.session_tokens,
786        )
787    }
788}
789
790fn engine_u64(snapshot: Option<&serde_json::Value>, key: &str) -> Option<u64> {
791    snapshot?.get(key)?.as_u64()
792}
793
794fn engine_bool(snapshot: Option<&serde_json::Value>, key: &str) -> Option<bool> {
795    snapshot?.get(key)?.as_bool()
796}
797
798fn engine_str<'a>(snapshot: Option<&'a serde_json::Value>, key: &str) -> Option<&'a str> {
799    snapshot?.get(key)?.as_str()
800}
801
802fn auto_config_health_value(auto_config: Option<&ResolvedFerrumConfig>) -> serde_json::Value {
803    match auto_config {
804        Some(auto_config) => auto_config.effective_config_document(),
805        None => {
806            match FerrumConfigBuilder::new(RuntimeConfigSnapshot::capture_current()).resolve() {
807                Ok(auto_config) => auto_config.effective_config_document(),
808                Err(err) => serde_json::json!({
809                    "schema_version": 1,
810                    "error": err.to_string(),
811                }),
812            }
813        }
814    }
815}
816
817fn admission_health_json(
818    engine_status: &EngineStatus,
819    scheduler_metrics: &EngineMetrics,
820    auto_config: &serde_json::Value,
821    runtime_snapshot: Option<&ferrum_types::ExecutorAdmissionSnapshot>,
822    runtime_error: Option<&str>,
823) -> serde_json::Value {
824    let configured = auto_config
825        .get("admission")
826        .and_then(|value| value.as_object());
827    let preflight_effective_max_concurrent = configured
828        .and_then(|value| value.get("effective_max_concurrent"))
829        .and_then(|value| value.as_u64());
830    let effective_max_concurrent = if runtime_error.is_some() {
831        None
832    } else {
833        Some(
834            runtime_snapshot
835                .map(|snapshot| u64::from(snapshot.maximum_active_sequences()))
836                .or(preflight_effective_max_concurrent)
837                .unwrap_or_else(|| {
838                    (engine_status.active_requests + engine_status.queued_requests)
839                        .max(1)
840                        .try_into()
841                        .unwrap_or(u64::MAX)
842                }),
843        )
844    };
845    let active_sequences = runtime_error.is_none().then(|| {
846        runtime_snapshot
847            .map(|snapshot| u64::from(snapshot.active_sequences()))
848            .unwrap_or_else(|| engine_status.active_requests as u64)
849    });
850    let waiting_requests = runtime_error.is_none().then(|| {
851        runtime_snapshot
852            .map(|snapshot| u64::from(snapshot.waiting_requests()))
853            .unwrap_or_else(|| engine_status.queued_requests as u64)
854    });
855    serde_json::json!({
856        "schema_version": 2,
857        "source": if runtime_error.is_some() {
858            "runtime_error"
859        } else if runtime_snapshot.is_some() {
860            "runtime_executor"
861        } else {
862            "startup_preflight_and_engine_status"
863        },
864        "runtime_snapshot_available": runtime_snapshot.is_some(),
865        "runtime_contract_error": runtime_error,
866        "resource_authority": runtime_snapshot
867            .and_then(|snapshot| serde_json::to_value(snapshot.resource_authority()).ok())
868            .unwrap_or(serde_json::Value::Null),
869        "effective_max_concurrent": effective_max_concurrent,
870        "maximum_active_sequences": runtime_snapshot
871            .map(|snapshot| u64::from(snapshot.maximum_active_sequences())),
872        "maximum_scheduled_tokens": runtime_snapshot
873            .map(|snapshot| snapshot.maximum_scheduled_tokens()),
874        "preflight_effective_max_concurrent": preflight_effective_max_concurrent,
875        "queue_depth": waiting_requests,
876        "active_sequences": active_sequences,
877        "active_prefill": runtime_snapshot
878            .map(|snapshot| u64::from(snapshot.active_prefill_sequences())),
879        "active_decode": runtime_snapshot
880            .map(|snapshot| u64::from(snapshot.active_decode_sequences())),
881        "current_batch_size": runtime_snapshot
882            .and_then(|snapshot| snapshot.current_batch_size())
883            .map(u64::from),
884        "capacity_blocked_requests": runtime_snapshot
885            .and_then(|snapshot| snapshot.capacity_blocked_requests())
886            .map(u64::from),
887        "rejected_requests_total": 0u64,
888        "failed_requests_total": scheduler_metrics.failed_requests,
889        "completed_requests_total": scheduler_metrics.successful_requests,
890        "avg_queue_wait_time_ms": scheduler_metrics.queue_metrics.avg_queue_wait_time_ms,
891        "scheduler_policy": configured
892            .and_then(|value| value.get("scheduler_policy"))
893            .and_then(|value| value.as_str())
894            .unwrap_or("unknown"),
895        "phase_detail_source": if runtime_snapshot.is_some() {
896            "scheduler_request_index_single_read"
897        } else {
898            "unavailable"
899        },
900    })
901}
902
903fn admission_prometheus_metrics(admission: &serde_json::Value) -> String {
904    let snapshot_available = u8::from(
905        admission
906            .get("runtime_snapshot_available")
907            .and_then(serde_json::Value::as_bool)
908            .unwrap_or(false),
909    );
910    let mut output = format!("ferrum_admission_runtime_snapshot_available {snapshot_available}\n");
911    for (field, metric) in [
912        (
913            "effective_max_concurrent",
914            "ferrum_admission_effective_max_concurrent",
915        ),
916        (
917            "maximum_active_sequences",
918            "ferrum_admission_maximum_active_sequences",
919        ),
920        (
921            "maximum_scheduled_tokens",
922            "ferrum_admission_maximum_scheduled_tokens",
923        ),
924        ("queue_depth", "ferrum_admission_queue_depth"),
925        (
926            "capacity_blocked_requests",
927            "ferrum_admission_capacity_blocked_requests",
928        ),
929        ("active_sequences", "ferrum_admission_active_sequences"),
930        ("active_prefill", "ferrum_admission_active_prefill"),
931        ("active_decode", "ferrum_admission_active_decode"),
932        ("current_batch_size", "ferrum_admission_current_batch_size"),
933        (
934            "rejected_requests_total",
935            "ferrum_admission_rejected_requests_total",
936        ),
937        (
938            "failed_requests_total",
939            "ferrum_admission_failed_requests_total",
940        ),
941        (
942            "completed_requests_total",
943            "ferrum_admission_completed_requests_total",
944        ),
945    ] {
946        if let Some(value) = admission.get(field).and_then(serde_json::Value::as_u64) {
947            output.push_str(&format!("{metric} {value}\n"));
948        }
949    }
950    output
951}
952
953fn request_session_id(headers: &HeaderMap, request: &ChatCompletionsRequest) -> Option<String> {
954    headers
955        .get(FERRUM_SESSION_HEADER)
956        .and_then(|value| value.to_str().ok())
957        .map(str::trim)
958        .filter(|value| !value.is_empty())
959        .map(str::to_string)
960        .or_else(|| {
961            request
962                .metadata
963                .as_ref()
964                .and_then(|metadata| metadata.get("ferrum_session_id"))
965                .and_then(|value| value.as_str())
966                .map(str::trim)
967                .filter(|value| !value.is_empty())
968                .map(str::to_string)
969        })
970}
971
972fn benchmark_request_correlation(
973    headers: &HeaderMap,
974) -> std::result::Result<Option<BenchmarkRequestCorrelation>, ServerError> {
975    let header_value = |name: &'static str| {
976        headers
977            .get(name)
978            .map(|value| {
979                value.to_str().map_err(|_| {
980                    ServerError::invalid_request(
981                        format!("{name} must contain visible ASCII text"),
982                        Some(name),
983                    )
984                })
985            })
986            .transpose()
987    };
988    BenchmarkRequestCorrelation::from_header_values(
989        header_value(BENCHMARK_RUN_ID_HEADER)?,
990        header_value(BENCHMARK_CELL_ID_HEADER)?,
991        header_value(BENCHMARK_REPEAT_INDEX_HEADER)?,
992        header_value(BENCHMARK_PHASE_HEADER)?,
993        header_value(BENCHMARK_REQUEST_INDEX_HEADER)?,
994    )
995    .map_err(|error| ServerError::invalid_request(error, Some(BENCHMARK_RUN_ID_HEADER)))
996}
997
998fn extend_benchmark_profile_attributes(
999    attributes: &mut BTreeMap<String, serde_json::Value>,
1000    correlation: Option<&BenchmarkRequestCorrelation>,
1001) {
1002    let Some(correlation) = correlation else {
1003        return;
1004    };
1005    attributes.extend([
1006        (
1007            "benchmark_run_id".to_string(),
1008            serde_json::json!(correlation.benchmark_run_id),
1009        ),
1010        (
1011            "cell_id".to_string(),
1012            serde_json::json!(correlation.cell_id),
1013        ),
1014        (
1015            "repeat_index".to_string(),
1016            serde_json::json!(correlation.repeat_index),
1017        ),
1018        (
1019            "phase".to_string(),
1020            serde_json::json!(correlation.phase.as_str()),
1021        ),
1022        (
1023            "request_index".to_string(),
1024            serde_json::json!(correlation.request_index),
1025        ),
1026    ]);
1027}
1028
1029fn approx_tokens(text: &str) -> usize {
1030    approx_tokens_for_chars(text.chars().count())
1031}
1032
1033fn approx_tokens_for_chars(chars: usize) -> usize {
1034    (chars / 4).max(1)
1035}
1036
1037fn longest_common_prefix_chars(a: &str, b: &str) -> usize {
1038    a.chars().zip(b.chars()).take_while(|(a, b)| a == b).count()
1039}
1040
1041fn trim_messages_to_token_budget(messages: &mut Vec<ChatMessage>, max_tokens: usize) {
1042    let max_tokens = max_tokens.max(1);
1043    while messages.len() > 1
1044        && messages
1045            .iter()
1046            .map(|msg| approx_tokens(&msg.content))
1047            .sum::<usize>()
1048            > max_tokens
1049    {
1050        messages.remove(0);
1051    }
1052}
1053
1054#[async_trait]
1055impl HttpServer for AxumServer {
1056    async fn start(&self, config: &ServerConfig) -> ferrum_types::Result<()> {
1057        if self.lifecycle.shutdown_requested.load(Ordering::Acquire) {
1058            return Err(Error::internal(
1059                "cannot start Axum server after shutdown was requested",
1060            ));
1061        }
1062        self.lifecycle
1063            .running
1064            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
1065            .map_err(|_| Error::internal("Axum server is already running"))?;
1066        let _run_guard = AxumServerRunGuard {
1067            lifecycle: Arc::clone(&self.lifecycle),
1068        };
1069        let addr = format!("{}:{}", config.host, config.port);
1070        info!("Starting Axum server on {}", addr);
1071
1072        let app = self.build_router_with_state(
1073            self.state
1074                .clone()
1075                .with_request_dump_dir(config.request_dump_dir.clone())
1076                .with_profile_jsonl(config.profile_jsonl.clone())
1077                .with_profile_detail(config.profile_detail)
1078                .with_memory_profile_jsonl(config.memory_profile_jsonl.clone()),
1079        );
1080        let listener = tokio::net::TcpListener::bind(&addr)
1081            .await
1082            .map_err(|e| Error::internal(format!("Failed to bind to {}: {}", addr, e)))?;
1083
1084        info!("Server listening on {}", addr);
1085
1086        let lifecycle = Arc::clone(&self.lifecycle);
1087        axum::serve(listener, app)
1088            .with_graceful_shutdown(async move { lifecycle.wait_for_shutdown().await })
1089            .await
1090            .map_err(|e| Error::internal(format!("Server error: {}", e)))?;
1091
1092        Ok(())
1093    }
1094
1095    async fn stop(&self, timeout: std::time::Duration) -> ferrum_types::Result<()> {
1096        let _stop_guard = self.lifecycle.stop_lock.lock().await;
1097        info!("Stopping Axum server");
1098        self.lifecycle.request_shutdown();
1099
1100        let mut first_error = None;
1101        if self.lifecycle.running.load(Ordering::Acquire) {
1102            if tokio::time::timeout(timeout, self.lifecycle.wait_until_stopped())
1103                .await
1104                .is_err()
1105            {
1106                first_error = Some(Error::internal(format!(
1107                    "Axum server did not drain within {} ms",
1108                    timeout.as_millis()
1109                )));
1110            }
1111        }
1112
1113        if !self.lifecycle.engines_stopped.load(Ordering::Acquire) {
1114            match tokio::time::timeout(timeout, self.shutdown_loaded_engines()).await {
1115                Ok(Ok(())) => {
1116                    self.lifecycle
1117                        .engines_stopped
1118                        .store(true, Ordering::Release);
1119                }
1120                Ok(Err(error)) => {
1121                    if first_error.is_none() {
1122                        first_error = Some(error);
1123                    }
1124                }
1125                Err(_) => {
1126                    if first_error.is_none() {
1127                        first_error = Some(Error::internal(format!(
1128                            "engine shutdown did not complete within {} ms",
1129                            timeout.as_millis()
1130                        )));
1131                    }
1132                }
1133            }
1134        }
1135
1136        first_error.map_or(Ok(()), Err)
1137    }
1138
1139    fn is_running(&self) -> bool {
1140        self.lifecycle.running.load(Ordering::Acquire)
1141    }
1142
1143    fn address(&self) -> Option<std::net::SocketAddr> {
1144        // For MVP, return configured address
1145        format!("{}:{}", self.config.host, self.config.port)
1146            .parse()
1147            .ok()
1148    }
1149
1150    fn register_handler(
1151        &mut self,
1152        _path: &str,
1153        _method: HttpMethod,
1154        _handler: Box<dyn crate::traits::RequestHandler>,
1155    ) {
1156        // For MVP, routes are static
1157        unimplemented!("Dynamic handler registration not implemented in MVP")
1158    }
1159
1160    fn register_middleware(&mut self, _middleware: Box<dyn crate::traits::Middleware>) {
1161        // For MVP, middleware is static
1162        unimplemented!("Dynamic middleware registration not implemented in MVP")
1163    }
1164
1165    fn get_metrics(&self) -> ServerMetrics {
1166        // Return empty metrics for MVP
1167        ServerMetrics {
1168            total_requests: 0,
1169            requests_by_endpoint: std::collections::HashMap::new(),
1170            requests_by_status: std::collections::HashMap::new(),
1171            avg_response_time_ms: 0.0,
1172            p95_response_time_ms: 0.0,
1173            p99_response_time_ms: 0.0,
1174            active_connections: 0,
1175            bytes_sent: 0,
1176            bytes_received: 0,
1177            error_rate: 0.0,
1178            uptime_seconds: 0,
1179        }
1180    }
1181
1182    async fn health_check(&self) -> HealthStatus {
1183        HealthStatus::Healthy
1184    }
1185}
1186
1187/// Main chat completions handler
1188async fn chat_completions_handler(
1189    State(state): State<AppState>,
1190    headers: HeaderMap,
1191    request: std::result::Result<Json<ChatCompletionsRequest>, JsonRejection>,
1192) -> std::result::Result<Response, ServerError> {
1193    chat_completions_handler_with_phases(State(state), headers, request, None).await
1194}
1195
1196async fn chat_completions_handler_with_phases(
1197    State(state): State<AppState>,
1198    headers: HeaderMap,
1199    request: std::result::Result<Json<ChatCompletionsRequest>, JsonRejection>,
1200    mut message_phases: Option<Vec<Option<AssistantMessagePhase>>>,
1201) -> std::result::Result<Response, ServerError> {
1202    let Json(mut request) = request.map_err(|error| {
1203        ServerError::invalid_request(
1204            format!(
1205                "invalid chat completions request: {}",
1206                json_rejection_detail(&error)
1207            ),
1208            None,
1209        )
1210    })?;
1211    let benchmark_correlation = benchmark_request_correlation(&headers)?;
1212    let cache_policy = CachePolicy::current();
1213    if message_phases
1214        .as_ref()
1215        .is_some_and(|phases| phases.len() != request.messages.len())
1216    {
1217        return Err(ServerError::InternalError(
1218            "Responses message phase metadata did not match input history".to_string(),
1219        ));
1220    }
1221    let session_context =
1222        state
1223            .cache
1224            .prepare_session_request(&mut request, &headers, &cache_policy);
1225    if let Some(phases) = &mut message_phases {
1226        let prepended = request
1227            .messages
1228            .len()
1229            .checked_sub(phases.len())
1230            .ok_or_else(|| {
1231                ServerError::InternalError(
1232                    "session preparation shortened Responses input history".to_string(),
1233                )
1234            })?;
1235        phases.splice(0..0, std::iter::repeat(None).take(prepended));
1236    }
1237
1238    let span = span!(Level::INFO, "chat_completions", model = %request.model);
1239    let _enter = span.enter();
1240
1241    info!(
1242        "Received chat completions request for model: {}",
1243        request.model
1244    );
1245    debug!("Request: {:?}", request);
1246
1247    // OpenAI spec requires at least one message. Reject empty arrays at
1248    // the boundary rather than synthesising a fake prompt downstream.
1249    validate_chat_request(&request)?;
1250    let (engine_model_id, lora_adapter) = resolve_request_model(
1251        &state.served_model_registry,
1252        &request.model,
1253        ServedModelKind::Llm,
1254    )?;
1255
1256    // Convert OpenAI request to internal format
1257    let mut inference_request = convert_chat_request_with_template_model_and_default(
1258        &request,
1259        &engine_model_id.0,
1260        state.prompt_template.as_deref(),
1261        state.default_enable_thinking,
1262        state.interleaved_system_coalescing.unwrap_or(true),
1263        message_phases.as_deref(),
1264    )
1265    .map_err(server_error_from_ferrum_error)?;
1266    apply_served_model_resolution(&mut inference_request, engine_model_id, lora_adapter);
1267    if state.request_dump_dir.is_some() {
1268        inference_request.evidence_request.capture_prompt_token_ids = true;
1269    }
1270    inference_request
1271        .evidence_request
1272        .capture_engine_token_timing = state.profile_detail.captures_engine_token_timing();
1273    state
1274        .cache
1275        .record_prefix_prompt(&inference_request.prompt, &cache_policy);
1276    if let Err(err) =
1277        write_chat_request_replay_bundle(&state, &headers, &request, &inference_request)
1278    {
1279        warn!("failed to write chat request replay bundle: {}", err);
1280    }
1281
1282    // Check if streaming is requested
1283    if request.stream.unwrap_or(false) {
1284        handle_chat_completions_stream(state, request, inference_request, benchmark_correlation)
1285            .await
1286    } else {
1287        handle_chat_completions_sync(
1288            state,
1289            request,
1290            inference_request,
1291            session_context,
1292            benchmark_correlation,
1293        )
1294        .await
1295    }
1296}
1297
1298fn json_rejection_detail(rejection: &JsonRejection) -> String {
1299    const MAX_DETAIL_CHARS: usize = 512;
1300
1301    let mut details = Vec::new();
1302    let mut current: Option<&(dyn StdError + 'static)> = Some(rejection);
1303    while let Some(error) = current {
1304        let detail = error.to_string();
1305        if !detail.is_empty() && details.last() != Some(&detail) {
1306            details.push(detail);
1307        }
1308        current = error.source();
1309    }
1310
1311    details.join(": ").chars().take(MAX_DETAIL_CHARS).collect()
1312}
1313
1314fn write_chat_request_replay_bundle(
1315    state: &AppState,
1316    headers: &HeaderMap,
1317    openai_request: &ChatCompletionsRequest,
1318    inference_request: &InferenceRequest,
1319) -> std::result::Result<(), String> {
1320    let Some(root) = state.request_dump_dir.as_ref() else {
1321        return Ok(());
1322    };
1323    let request_id = inference_request.id.to_string();
1324    let bundle_dir = root.join(&request_id);
1325    fs::create_dir_all(&bundle_dir).map_err(|err| err.to_string())?;
1326
1327    let sanitized_body = sanitized_chat_request_body(openai_request);
1328    let replay_body_path = bundle_dir.join("replay_body.json");
1329    write_json_value(&replay_body_path, &sanitized_body)?;
1330    let engine_replay_argv = replay_bundle_argv(&bundle_dir);
1331    let output_text_body = format!(
1332        "[server request replay emitted before response]\nsha256={}\nchars=0\n",
1333        sha256_hex(b"")
1334    );
1335
1336    let request = serde_json::json!({
1337        "schema_version": OBSERVABILITY_PROFILE_SCHEMA_VERSION,
1338        "entrypoint": "serve",
1339        "request_id": request_id,
1340        "model": openai_request.model.clone(),
1341        "backend": "actual",
1342        "endpoint": "/v1/chat/completions",
1343        "method": "POST",
1344        "stream": openai_request.stream.unwrap_or(false),
1345        "actual_model_smoke": true,
1346        "sanitized": true,
1347        "http": {
1348            "method": "POST",
1349            "path": "/v1/chat/completions",
1350            "headers": sanitized_replay_headers(headers),
1351            "body": sanitized_body
1352        }
1353    });
1354    let files = [
1355        ("request.json", request),
1356        (
1357            "prompt_token_ids.json",
1358            serde_json::json!({
1359                "schema_version": OBSERVABILITY_PROFILE_SCHEMA_VERSION,
1360                "request_id": request_id,
1361                "model": openai_request.model.clone(),
1362                "tokenizer_or_model": openai_request.model.clone(),
1363                "token_ids": null,
1364                "token_count": null,
1365                "unavailable_reason": "server request replay captures the OpenAI body before prompt token ids are retained",
1366                "sanitized": true
1367            }),
1368        ),
1369        (
1370            "sampling_params.json",
1371            serde_json::json!({
1372                "schema_version": OBSERVABILITY_PROFILE_SCHEMA_VERSION,
1373                "request_id": request_id,
1374                "sampling_params": inference_request.sampling_params.clone(),
1375                "unavailable_reason": null
1376            }),
1377        ),
1378        (
1379            "runtime_effective_config.json",
1380            serde_json::json!({
1381                "schema_version": OBSERVABILITY_PROFILE_SCHEMA_VERSION,
1382                "request_id": request_id,
1383                "entrypoint": "serve",
1384                "endpoint": "/v1/chat/completions",
1385                "stream": openai_request.stream.unwrap_or(false),
1386                "request_dump_dir": root.to_string_lossy(),
1387                "sanitized": true
1388            }),
1389        ),
1390        (
1391            "backend_selection.json",
1392            serde_json::json!({
1393                "schema_version": OBSERVABILITY_PROFILE_SCHEMA_VERSION,
1394                "request_id": request_id,
1395                "backend": "actual",
1396                "model": openai_request.model.clone(),
1397                "actual_model_smoke": true
1398            }),
1399        ),
1400        (
1401            "output_token_ids.json",
1402            serde_json::json!({
1403                "schema_version": OBSERVABILITY_PROFILE_SCHEMA_VERSION,
1404                "request_id": request_id,
1405                "token_ids": [],
1406                "token_count": 0,
1407                "finish_reason": null,
1408                "unavailable_reason": "server request replay bundle is emitted at request admission in this WP9 slice"
1409            }),
1410        ),
1411        (
1412            "bad_output_scan.json",
1413            serde_json::json!({
1414                "schema_version": OBSERVABILITY_PROFILE_SCHEMA_VERSION,
1415                "request_id": request_id,
1416                "bad_output": false,
1417                "bad_text_count": 0,
1418                "reasons": [],
1419                "first_bad_text_span": null,
1420                "failure_kind": null,
1421                "output_chars": 0,
1422                "classified_output_sha256": sha256_hex(b""),
1423                "output_sha256": sha256_hex(output_text_body.as_bytes())
1424            }),
1425        ),
1426        (
1427            "replay.command.json",
1428            serde_json::json!({
1429                "schema_version": OBSERVABILITY_PROFILE_SCHEMA_VERSION,
1430                "request_id": request_id,
1431                "entrypoint": "serve",
1432                "command": replay_curl_command(&bundle_dir),
1433                "argv": replay_curl_argv(&bundle_dir),
1434                "bundle_dir": bundle_dir.to_string_lossy(),
1435                "requires_running_server": true,
1436                "engine_replay": {
1437                    "mode": "bundle_offline",
1438                    "requires_http_server": false,
1439                    "command": shell_command(&engine_replay_argv),
1440                    "argv": engine_replay_argv
1441                },
1442                "sanitized": true
1443            }),
1444        ),
1445    ];
1446    for (name, value) in files {
1447        write_json_value(&bundle_dir.join(name), &value)?;
1448    }
1449    fs::write(bundle_dir.join("output_text.txt"), output_text_body)
1450        .map_err(|err| err.to_string())?;
1451    Ok(())
1452}
1453
1454fn write_chat_request_failure_diagnostics(
1455    state: &AppState,
1456    request_id: &str,
1457    failure_kind: &str,
1458    phase: &str,
1459    error_kind: &str,
1460    message: &str,
1461    engine_status: Option<&EngineStatus>,
1462) -> std::result::Result<(), String> {
1463    let admission_summary = state
1464        .auto_config
1465        .as_ref()
1466        .map(|config| config.admission_summary_document());
1467    write_chat_request_failure_diagnostics_at_root(
1468        state.request_dump_dir.as_ref().map(|root| root.as_path()),
1469        admission_summary.as_ref(),
1470        engine_status,
1471        request_id,
1472        failure_kind,
1473        phase,
1474        error_kind,
1475        message,
1476    )
1477}
1478
1479fn write_chat_request_completion_replay_bundle(
1480    request_dump_dir: Option<&Path>,
1481    request_id: &str,
1482    output_text: &str,
1483    output_token_ids: &[TokenId],
1484    finish_reason: Option<&str>,
1485) -> std::result::Result<(), String> {
1486    let Some(root) = request_dump_dir else {
1487        return Ok(());
1488    };
1489    let bundle_dir = root.join(request_id);
1490    fs::create_dir_all(&bundle_dir).map_err(|err| err.to_string())?;
1491    let token_ids = output_token_ids
1492        .iter()
1493        .map(|token| token.get())
1494        .collect::<Vec<_>>();
1495    let output_text_body = format!(
1496        "[redacted actual output]\nsha256={}\nchars={}\n",
1497        sha256_hex(output_text.as_bytes()),
1498        output_text.chars().count()
1499    );
1500    write_json_value(
1501        &bundle_dir.join("output_token_ids.json"),
1502        &serde_json::json!({
1503            "schema_version": OBSERVABILITY_PROFILE_SCHEMA_VERSION,
1504            "request_id": request_id,
1505            "token_ids": token_ids,
1506            "token_count": output_token_ids.len(),
1507            "finish_reason": finish_reason,
1508            "unavailable_reason": null
1509        }),
1510    )?;
1511    write_json_value(
1512        &bundle_dir.join("bad_output_scan.json"),
1513        &bad_output_scan_json(request_id, output_text, None, output_text_body.as_bytes()),
1514    )?;
1515    fs::write(bundle_dir.join("output_text.txt"), output_text_body)
1516        .map_err(|err| err.to_string())?;
1517    Ok(())
1518}
1519
1520fn write_chat_prompt_token_evidence(
1521    request_dump_dir: Option<&Path>,
1522    request_id: &str,
1523    model: &str,
1524    execution_evidence: Option<&InferenceExecutionEvidence>,
1525) -> std::result::Result<(), String> {
1526    let (Some(root), Some(evidence)) = (request_dump_dir, execution_evidence) else {
1527        return Ok(());
1528    };
1529    let bundle_dir = root.join(request_id);
1530    fs::create_dir_all(&bundle_dir).map_err(|err| err.to_string())?;
1531    let prompt_token_ids = evidence
1532        .prompt_token_ids
1533        .iter()
1534        .map(|token| token.get())
1535        .collect::<Vec<_>>();
1536    write_json_value(
1537        &bundle_dir.join("prompt_token_ids.json"),
1538        &serde_json::json!({
1539            "schema_version": OBSERVABILITY_PROFILE_SCHEMA_VERSION,
1540            "request_id": request_id,
1541            "model": model,
1542            "tokenizer_or_model": model,
1543            "token_ids": prompt_token_ids,
1544            "token_count": evidence.prompt_token_ids.len(),
1545            "unavailable_reason": null,
1546            "sanitized": true
1547        }),
1548    )
1549}
1550
1551#[derive(Clone, Copy, Default)]
1552struct ChatRequestProfileTiming<'a> {
1553    engine_evidence: Option<&'a InferenceExecutionEvidence>,
1554    first_engine_chunk_received_us: Option<u64>,
1555    first_sse_enqueue_us: Option<u64>,
1556}
1557
1558#[allow(clippy::too_many_arguments)]
1559fn write_chat_request_profile_event(
1560    state: &AppState,
1561    request_id: &str,
1562    benchmark_correlation: Option<&BenchmarkRequestCorrelation>,
1563    model: &str,
1564    stream: bool,
1565    phase: &str,
1566    started_at: Instant,
1567    timing: ChatRequestProfileTiming<'_>,
1568    output_token_count: usize,
1569    usage: Option<&TokenUsage>,
1570    finish_reason: Option<&str>,
1571    error: Option<ProfileError>,
1572) -> std::result::Result<(), String> {
1573    let Some(path) = state.profile_jsonl.as_ref() else {
1574        return Ok(());
1575    };
1576    let timestamp = chrono::Utc::now();
1577    let status = if error.is_some() {
1578        ProfileStatus::Failure
1579    } else {
1580        ProfileStatus::Ok
1581    };
1582    let duration_us = elapsed_us_since(started_at);
1583    let mut attributes = BTreeMap::from([
1584        ("actual_model_smoke".to_string(), serde_json::json!(true)),
1585        (
1586            "diagnostic_only".to_string(),
1587            serde_json::json!(state.profile_detail.diagnostic_only()),
1588        ),
1589        (
1590            "endpoint".to_string(),
1591            serde_json::json!("/v1/chat/completions"),
1592        ),
1593        (
1594            "e2e_duration_us".to_string(),
1595            serde_json::json!(duration_us),
1596        ),
1597        ("l0_only".to_string(), serde_json::json!(false)),
1598        (
1599            "profile_detail".to_string(),
1600            serde_json::json!(state.profile_detail.as_str()),
1601        ),
1602        ("stream".to_string(), serde_json::json!(stream)),
1603        (
1604            "output_token_count".to_string(),
1605            serde_json::json!(output_token_count),
1606        ),
1607        (
1608            "execution_request_id".to_string(),
1609            serde_json::json!(format!("request.product.{request_id}")),
1610        ),
1611    ]);
1612    extend_benchmark_profile_attributes(&mut attributes, benchmark_correlation);
1613    if let Some(usage) = usage {
1614        attributes.insert(
1615            "prompt_token_count".to_string(),
1616            serde_json::json!(usage.prompt_tokens),
1617        );
1618        attributes.insert(
1619            "completion_token_count".to_string(),
1620            serde_json::json!(usage.completion_tokens),
1621        );
1622        attributes.insert(
1623            "total_token_count".to_string(),
1624            serde_json::json!(usage.total_tokens),
1625        );
1626        attributes.insert("token_count_source".to_string(), serde_json::json!("usage"));
1627    } else {
1628        attributes.insert(
1629            "completion_token_count".to_string(),
1630            serde_json::json!(output_token_count),
1631        );
1632        attributes.insert(
1633            "total_token_count".to_string(),
1634            serde_json::json!(output_token_count),
1635        );
1636        attributes.insert(
1637            "token_count_source".to_string(),
1638            serde_json::json!("generated_tokens"),
1639        );
1640    }
1641    if let Some(engine_timing) = timing
1642        .engine_evidence
1643        .and_then(|evidence| evidence.engine_token_timing.as_ref())
1644    {
1645        engine_timing
1646            .validate(output_token_count)
1647            .map_err(|error| format!("invalid engine token timing evidence: {error}"))?;
1648        attributes.extend(ferrum_types::engine_token_timing_profile_attributes(
1649            engine_timing,
1650        ));
1651    } else if status == ProfileStatus::Ok && state.profile_detail.captures_engine_token_timing() {
1652        return Err(format!(
1653            "{} profile completed without required engine token timing evidence",
1654            state.profile_detail.as_str()
1655        ));
1656    }
1657    if let Some(received_us) = timing.first_engine_chunk_received_us {
1658        attributes.insert(
1659            "engine_stream_first_chunk_received_us".to_string(),
1660            serde_json::json!(received_us),
1661        );
1662    }
1663    if let Some(enqueue_us) = timing.first_sse_enqueue_us {
1664        attributes.insert(
1665            "http_first_sse_enqueue_us".to_string(),
1666            serde_json::json!(enqueue_us),
1667        );
1668    }
1669    if stream {
1670        attributes.insert(
1671            "http_stream_flush_unavailable_reason".to_string(),
1672            serde_json::json!(
1673                "socket flush completion is outside the axum handler observation boundary"
1674            ),
1675        );
1676    }
1677    if let Some(reason) = finish_reason {
1678        attributes.insert("finish_reason".to_string(), serde_json::json!(reason));
1679    }
1680    if let Some(error) = error.as_ref() {
1681        attributes.insert(
1682            if error.blocking {
1683                "first_failure_event"
1684            } else {
1685                "terminal_failure_event"
1686            }
1687            .to_string(),
1688            serde_json::json!(true),
1689        );
1690    }
1691
1692    let replay = state.request_dump_dir.as_ref().map(|root| {
1693        let bundle_dir = root.join(request_id);
1694        ReplayReference {
1695            command: replay_curl_command(&bundle_dir),
1696            bundle_dir: Some(root.to_string_lossy().to_string()),
1697        }
1698    });
1699    let resource = error.as_ref().map(|error| ResourceTraceEvent {
1700        owner_kind: "request".to_string(),
1701        owner_id: request_id.to_string(),
1702        resource_kind: "chat_request".to_string(),
1703        action: ResourceAction::Reject,
1704        amount: None,
1705        before: None,
1706        after: None,
1707        capacity: Some(1),
1708        underflow_amount: None,
1709        reason: Some(error.message.clone()),
1710        error_kind: Some(error.kind.clone()),
1711        message: Some(error.message.clone()),
1712        resource_error_kind: Some(error.kind.clone()),
1713    });
1714    let event = FerrumProfileEvent {
1715        schema_version: OBSERVABILITY_PROFILE_SCHEMA_VERSION,
1716        ts_unix_nanos: timestamp
1717            .timestamp_nanos_opt()
1718            .unwrap_or_else(|| timestamp.timestamp_micros() * 1_000),
1719        event_id: format!(
1720            "evt-server-chat-{}-{request_id}",
1721            if stream { "stream" } else { "sync" }
1722        ),
1723        request_id: request_id.to_string(),
1724        correlation_id: Some(request_id.to_string()),
1725        entrypoint: ProfileEntrypoint::Serve,
1726        backend: "actual".to_string(),
1727        runtime_preset_hash: state
1728            .auto_config
1729            .as_ref()
1730            .map(ResolvedFerrumConfig::runtime_env_hash)
1731            .unwrap_or_else(|| format!("sha256:{}", sha256_hex(b"serve-profile"))),
1732        phase: phase.to_string(),
1733        event_kind: ProfileEventKind::TimedSpan,
1734        timestamp,
1735        status,
1736        model: Some(model.to_string()),
1737        duration_us: Some(duration_us),
1738        memory: None,
1739        resource,
1740        error,
1741        replay,
1742        shape: BTreeMap::from([("batch_size".to_string(), serde_json::json!(1))]),
1743        backend_detail: None,
1744        attributes,
1745    };
1746    append_profile_event(path.as_path(), &event)
1747}
1748
1749fn maybe_write_first_request_memory_stage(
1750    state: &AppState,
1751    request_id: &str,
1752    benchmark_correlation: Option<&BenchmarkRequestCorrelation>,
1753    model: &str,
1754    stream: bool,
1755    started_at: Instant,
1756    before: Option<ProcessMemorySample>,
1757) -> std::result::Result<(), String> {
1758    if state.profile_jsonl.is_none() && state.memory_profile_jsonl.is_none() {
1759        return Ok(());
1760    }
1761    if state
1762        .first_request_memory_recorded
1763        .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
1764        .is_err()
1765    {
1766        return Ok(());
1767    }
1768    let after = ProcessMemorySampler.sample();
1769    let memory = after.map(|after| ProcessMemoryObservation::from_samples(before, after));
1770    let timestamp = chrono::Utc::now();
1771    let mut attributes = BTreeMap::from([
1772        ("actual_model_smoke".to_string(), serde_json::json!(true)),
1773        (
1774            "diagnostic_only".to_string(),
1775            serde_json::json!(state.profile_detail.diagnostic_only()),
1776        ),
1777        (
1778            "endpoint".to_string(),
1779            serde_json::json!("/v1/chat/completions"),
1780        ),
1781        ("l0_only".to_string(), serde_json::json!(false)),
1782        (
1783            "memory_stage".to_string(),
1784            serde_json::json!("first_request_done"),
1785        ),
1786        (
1787            "profile_detail".to_string(),
1788            serde_json::json!(state.profile_detail.as_str()),
1789        ),
1790        ("stream".to_string(), serde_json::json!(stream)),
1791    ]);
1792    extend_benchmark_profile_attributes(&mut attributes, benchmark_correlation);
1793    let memory_snapshot = if let Some(memory) = &memory {
1794        attributes.insert(
1795            "memory_measurement".to_string(),
1796            serde_json::json!("process_rss"),
1797        );
1798        attributes.insert(
1799            "process_memory_source".to_string(),
1800            serde_json::json!(memory.source),
1801        );
1802        memory.to_snapshot("process", Some("actual"))
1803    } else {
1804        attributes.insert(
1805            "memory_measurement".to_string(),
1806            serde_json::json!("not_collected"),
1807        );
1808        ferrum_types::MemorySnapshot {
1809            scope: "process".to_string(),
1810            backend: Some("actual".to_string()),
1811            before_bytes: Some(0),
1812            after_bytes: Some(0),
1813            current_bytes: Some(0),
1814            high_water_bytes: Some(0),
1815            available_bytes: None,
1816        }
1817    };
1818    let replay = state.request_dump_dir.as_ref().map(|root| {
1819        let bundle_dir = root.join(request_id);
1820        ReplayReference {
1821            command: replay_curl_command(&bundle_dir),
1822            bundle_dir: Some(root.to_string_lossy().to_string()),
1823        }
1824    });
1825    let event = FerrumProfileEvent {
1826        schema_version: OBSERVABILITY_PROFILE_SCHEMA_VERSION,
1827        ts_unix_nanos: timestamp
1828            .timestamp_nanos_opt()
1829            .unwrap_or_else(|| timestamp.timestamp_micros() * 1_000),
1830        event_id: format!("evt-server-chat-memory-first-request-{request_id}"),
1831        request_id: request_id.to_string(),
1832        correlation_id: Some(request_id.to_string()),
1833        entrypoint: ProfileEntrypoint::Serve,
1834        backend: "actual".to_string(),
1835        runtime_preset_hash: state
1836            .auto_config
1837            .as_ref()
1838            .map(ResolvedFerrumConfig::runtime_env_hash)
1839            .unwrap_or_else(|| format!("sha256:{}", sha256_hex(b"serve-profile"))),
1840        phase: "actual_serve_first_request_done".to_string(),
1841        event_kind: ProfileEventKind::Memory,
1842        timestamp,
1843        status: ProfileStatus::Ok,
1844        model: Some(model.to_string()),
1845        duration_us: Some(elapsed_us_since(started_at)),
1846        memory: Some(memory_snapshot),
1847        resource: None,
1848        error: None,
1849        replay,
1850        shape: BTreeMap::from([("batch_size".to_string(), serde_json::json!(1))]),
1851        backend_detail: None,
1852        attributes,
1853    };
1854    if let Some(path) = &state.profile_jsonl {
1855        append_profile_event(path.as_path(), &event)?;
1856    }
1857    if let Some(path) = &state.memory_profile_jsonl {
1858        append_profile_event(path.as_path(), &event)?;
1859    }
1860    Ok(())
1861}
1862
1863fn request_memory_sample_before(state: &AppState) -> Option<ProcessMemorySample> {
1864    (state.profile_jsonl.is_some() || state.memory_profile_jsonl.is_some())
1865        .then(|| ProcessMemorySampler.sample())
1866        .flatten()
1867}
1868
1869fn append_profile_event(
1870    path: &Path,
1871    event: &FerrumProfileEvent,
1872) -> std::result::Result<(), String> {
1873    event.validate().map_err(|err| err.to_string())?;
1874    ferrum_bench_core::write_jsonl_records(
1875        path,
1876        ferrum_bench_core::JsonlJournalOpenMode::Append,
1877        std::slice::from_ref(event),
1878    )
1879    .map_err(|error| error.to_string())
1880}
1881
1882fn elapsed_us_since(started_at: Instant) -> u64 {
1883    started_at
1884        .elapsed()
1885        .as_micros()
1886        .max(1)
1887        .try_into()
1888        .unwrap_or(u64::MAX)
1889}
1890
1891fn write_chat_request_failure_diagnostics_at_root(
1892    request_dump_dir: Option<&Path>,
1893    admission_summary: Option<&serde_json::Value>,
1894    engine_status: Option<&EngineStatus>,
1895    request_id: &str,
1896    failure_kind: &str,
1897    phase: &str,
1898    error_kind: &str,
1899    message: &str,
1900) -> std::result::Result<(), String> {
1901    let Some(root) = request_dump_dir else {
1902        return Ok(());
1903    };
1904    let bundle_dir = root.join(request_id);
1905    fs::create_dir_all(&bundle_dir).map_err(|err| err.to_string())?;
1906    let message = sanitize_diagnostic_text(message);
1907    let now = chrono::Utc::now();
1908
1909    let bad_scan_path = bundle_dir.join("bad_output_scan.json");
1910    let mut bad_scan = fs::read_to_string(&bad_scan_path)
1911        .ok()
1912        .and_then(|text| serde_json::from_str::<serde_json::Value>(&text).ok())
1913        .filter(|value| value.is_object())
1914        .unwrap_or_else(|| serde_json::json!({}));
1915    let bad_scan_obj = bad_scan
1916        .as_object_mut()
1917        .expect("bad scan fallback should be an object");
1918    bad_scan_obj.insert(
1919        "schema_version".to_string(),
1920        serde_json::json!(OBSERVABILITY_PROFILE_SCHEMA_VERSION),
1921    );
1922    bad_scan_obj.insert("request_id".to_string(), serde_json::json!(request_id));
1923    bad_scan_obj
1924        .entry("bad_output".to_string())
1925        .or_insert_with(|| serde_json::json!(false));
1926    bad_scan_obj
1927        .entry("bad_text_count".to_string())
1928        .or_insert_with(|| serde_json::json!(0));
1929    bad_scan_obj
1930        .entry("reasons".to_string())
1931        .or_insert_with(|| serde_json::json!([]));
1932    bad_scan_obj
1933        .entry("first_bad_text_span".to_string())
1934        .or_insert(serde_json::Value::Null);
1935    bad_scan_obj.insert("failure_kind".to_string(), serde_json::json!(failure_kind));
1936    bad_scan_obj.insert("failure_phase".to_string(), serde_json::json!(phase));
1937    bad_scan_obj.insert("error_kind".to_string(), serde_json::json!(error_kind));
1938    bad_scan_obj
1939        .entry("output_chars".to_string())
1940        .or_insert_with(|| serde_json::json!(0));
1941    bad_scan_obj
1942        .entry("output_sha256".to_string())
1943        .or_insert_with(|| serde_json::json!(sha256_hex(b"")));
1944    write_json_value(&bad_scan_path, &bad_scan)?;
1945
1946    let diagnostics = if chat_resource_failure_kind(failure_kind) {
1947        chat_resource_failure_diagnostics(
1948            request_id,
1949            failure_kind,
1950            phase,
1951            error_kind,
1952            &message,
1953            now.timestamp_millis(),
1954            admission_summary,
1955            engine_status,
1956        )
1957    } else {
1958        serde_json::json!({
1959            "schema_version": OBSERVABILITY_PROFILE_SCHEMA_VERSION,
1960            "entrypoint": "serve",
1961            "request_id": request_id,
1962            "failure_kind": failure_kind,
1963            "phase": phase,
1964            "first_failure_event": {
1965                "schema_version": OBSERVABILITY_PROFILE_SCHEMA_VERSION,
1966                "entrypoint": "serve",
1967                "request_id": request_id,
1968                "phase": phase,
1969                "error_kind": error_kind,
1970                "message": message,
1971                "timestamp_unix_ms": now.timestamp_millis()
1972            },
1973            "nearest_request_id": request_id,
1974            "log_excerpt": format!("{phase}: {message}"),
1975            "backtrace_excerpt": null,
1976            "nearest_resource_event": null,
1977            "nearest_memory_snapshot": null
1978        })
1979    };
1980    write_json_value(&bundle_dir.join("failure_diagnostics.json"), &diagnostics)?;
1981    Ok(())
1982}
1983
1984fn chat_resource_failure_diagnostics(
1985    request_id: &str,
1986    failure_kind: &str,
1987    phase: &str,
1988    error_kind: &str,
1989    message: &str,
1990    timestamp_unix_ms: i64,
1991    admission_summary: Option<&serde_json::Value>,
1992    engine_status: Option<&EngineStatus>,
1993) -> serde_json::Value {
1994    let resource_kind = chat_resource_kind_for_failure(failure_kind);
1995    let memory = engine_status
1996        .map(|status| &status.memory_usage)
1997        .map(|memory| {
1998            let current = memory.used_bytes as i64;
1999            let high_water = current.max(0);
2000            serde_json::json!({
2001                "scope": "serve_failure",
2002                "backend": "engine_status",
2003                "current_bytes": current.max(0),
2004                "high_water_bytes": high_water,
2005                "total_bytes": memory.total_bytes,
2006                "free_bytes": memory.free_bytes,
2007                "gpu_memory_bytes": memory.gpu_memory_bytes,
2008                "cpu_memory_bytes": memory.cpu_memory_bytes,
2009                "source": "engine_status"
2010            })
2011        })
2012        .unwrap_or_else(|| {
2013            serde_json::json!({
2014                "scope": "serve_failure",
2015                "backend": "engine_status",
2016                "current_bytes": 0,
2017                "high_water_bytes": 0,
2018                "source": "not_collected"
2019            })
2020        });
2021    let capacity = chat_failure_capacity(resource_kind, admission_summary, engine_status, message);
2022    let needed = capacity
2023        .get("needed")
2024        .and_then(|value| value.as_i64())
2025        .unwrap_or(1)
2026        .max(1);
2027    let capacity_value = capacity
2028        .get("capacity")
2029        .and_then(|value| value.as_i64())
2030        .unwrap_or(0)
2031        .max(0);
2032    serde_json::json!({
2033        "schema_version": OBSERVABILITY_PROFILE_SCHEMA_VERSION,
2034        "entrypoint": "serve",
2035        "request_id": request_id,
2036        "failure_kind": failure_kind,
2037        "phase": phase,
2038        "first_failure_event": {
2039            "schema_version": OBSERVABILITY_PROFILE_SCHEMA_VERSION,
2040            "entrypoint": "serve",
2041            "request_id": request_id,
2042            "phase": phase,
2043            "error_kind": error_kind,
2044            "message": message,
2045            "timestamp_unix_ms": timestamp_unix_ms
2046        },
2047        "nearest_request_id": request_id,
2048        "log_excerpt": format!("{phase}: {message}"),
2049        "capacity": capacity,
2050        "nearest_resource_event": {
2051            "owner_kind": "request",
2052            "owner_id": request_id,
2053            "resource_kind": resource_kind,
2054            "action": "reject",
2055            "amount": needed,
2056            "before": capacity_value,
2057            "after": capacity_value,
2058            "capacity": capacity_value,
2059            "reason": message
2060        },
2061        "nearest_memory_snapshot": memory
2062    })
2063}
2064
2065fn chat_failure_capacity(
2066    resource_kind: &str,
2067    admission_summary: Option<&serde_json::Value>,
2068    engine_status: Option<&EngineStatus>,
2069    reason: &str,
2070) -> serde_json::Value {
2071    if resource_kind == "device_memory" {
2072        let (needed, available, capacity) = engine_status
2073            .map(|status| {
2074                let memory = &status.memory_usage;
2075                let used = memory.used_bytes as i64;
2076                let available = memory.free_bytes as i64;
2077                let capacity = memory.total_bytes as i64;
2078                (
2079                    used.saturating_add(1).max(1),
2080                    available.max(0),
2081                    capacity.max(0),
2082                )
2083            })
2084            .unwrap_or((1, 0, 0));
2085        return serde_json::json!({
2086            "resource_kind": resource_kind,
2087            "needed": needed,
2088            "available": available,
2089            "capacity": capacity,
2090            "reason": reason
2091        });
2092    }
2093    let capacity = admission_summary
2094        .and_then(|summary| summary.get("effective_max_concurrent"))
2095        .and_then(|value| {
2096            value
2097                .as_i64()
2098                .or_else(|| value.as_u64().map(|value| value as i64))
2099        })
2100        .unwrap_or_else(|| {
2101            engine_status
2102                .map(|status| {
2103                    (status.active_requests as i64)
2104                        .saturating_add(status.queued_requests as i64)
2105                        .saturating_add(1)
2106                })
2107                .unwrap_or(0)
2108        })
2109        .max(0);
2110    let used = engine_status
2111        .map(|status| (status.active_requests as i64).saturating_add(status.queued_requests as i64))
2112        .unwrap_or(0)
2113        .max(0);
2114    serde_json::json!({
2115        "resource_kind": resource_kind,
2116        "needed": 1,
2117        "available": capacity.saturating_sub(used),
2118        "capacity": capacity,
2119        "reason": reason
2120    })
2121}
2122
2123fn chat_resource_failure_kind(failure_kind: &str) -> bool {
2124    matches!(
2125        failure_kind,
2126        "oom" | "prevented_oom" | "admission" | "admission_reject" | "oom_admission"
2127    )
2128}
2129
2130fn chat_resource_kind_for_failure(failure_kind: &str) -> &'static str {
2131    match failure_kind {
2132        "oom" | "prevented_oom" => "device_memory",
2133        _ => "admission_capacity",
2134    }
2135}
2136
2137fn sanitize_diagnostic_text(message: &str) -> String {
2138    let trimmed = message.trim();
2139    if trimmed.is_empty() {
2140        return "generation failed without an error message".to_string();
2141    }
2142    let lower = trimmed.to_ascii_lowercase();
2143    if lower.contains("authorization")
2144        || lower.contains("cookie")
2145        || lower.contains("api_key")
2146        || lower.contains("access_token")
2147        || lower.contains("refresh_token")
2148        || lower.contains("password")
2149        || trimmed.contains("sk-")
2150    {
2151        return "[redacted diagnostic message]".to_string();
2152    }
2153    trimmed.chars().take(2048).collect()
2154}
2155
2156fn sanitized_replay_headers(headers: &HeaderMap) -> serde_json::Value {
2157    let mut result = serde_json::Map::new();
2158    for key in ["content-type", "traceparent", "tracestate"] {
2159        if let Some(value) = headers.get(key).and_then(|value| value.to_str().ok()) {
2160            result.insert(key.to_string(), serde_json::json!(value));
2161        }
2162    }
2163    result.insert("authorization".to_string(), serde_json::json!("[redacted]"));
2164    result.insert("cookie".to_string(), serde_json::json!("[redacted]"));
2165    serde_json::Value::Object(result)
2166}
2167
2168fn sanitized_chat_request_body(request: &ChatCompletionsRequest) -> serde_json::Value {
2169    let mut value = serde_json::to_value(request).unwrap_or_else(|_| {
2170        serde_json::json!({
2171            "model": request.model.clone(),
2172            "stream": request.stream.unwrap_or(false),
2173            "messages": []
2174        })
2175    });
2176    redact_json_value(&mut value, None);
2177    value
2178}
2179
2180fn redact_json_value(value: &mut serde_json::Value, key: Option<&str>) {
2181    if key.is_some_and(is_secret_key) {
2182        *value = serde_json::json!("[redacted]");
2183        return;
2184    }
2185    if matches!(key, Some("content" | "arguments")) && value.is_string() {
2186        *value = serde_json::json!("[redacted]");
2187        return;
2188    }
2189    match value {
2190        serde_json::Value::Object(map) => {
2191            for field in ["content", "arguments"] {
2192                if let Some(chars) = map
2193                    .get(field)
2194                    .and_then(|child| child.as_str())
2195                    .map(|text| text.chars().count())
2196                {
2197                    map.insert(field.to_string(), serde_json::json!("[redacted]"));
2198                    map.insert(format!("{field}_redacted"), serde_json::json!(true));
2199                    map.insert(format!("{field}_chars"), serde_json::json!(chars));
2200                }
2201            }
2202            for (child_key, child) in map.iter_mut() {
2203                redact_json_value(child, Some(child_key.as_str()));
2204            }
2205        }
2206        serde_json::Value::Array(items) => {
2207            for child in items {
2208                redact_json_value(child, None);
2209            }
2210        }
2211        _ => {}
2212    }
2213}
2214
2215fn is_secret_key(key: &str) -> bool {
2216    let normalized = key
2217        .chars()
2218        .filter(|ch| *ch != '-' && *ch != '_')
2219        .flat_map(char::to_lowercase)
2220        .collect::<String>();
2221    matches!(
2222        normalized.as_str(),
2223        "authorization"
2224            | "cookie"
2225            | "secret"
2226            | "apikey"
2227            | "password"
2228            | "accesstoken"
2229            | "refreshtoken"
2230            | "idtoken"
2231    )
2232}
2233
2234fn replay_curl_argv(bundle_dir: &Path) -> Vec<String> {
2235    vec![
2236        "curl".to_string(),
2237        "-sS".to_string(),
2238        "-X".to_string(),
2239        "POST".to_string(),
2240        "http://127.0.0.1:8000/v1/chat/completions".to_string(),
2241        "-H".to_string(),
2242        "content-type: application/json".to_string(),
2243        "--data-binary".to_string(),
2244        format!("@{}", bundle_dir.join("replay_body.json").display()),
2245    ]
2246}
2247
2248fn replay_curl_command(bundle_dir: &Path) -> String {
2249    shell_command(&replay_curl_argv(bundle_dir))
2250}
2251
2252fn replay_bundle_argv(bundle_dir: &Path) -> Vec<String> {
2253    vec![
2254        "cargo".to_string(),
2255        "run".to_string(),
2256        "-p".to_string(),
2257        "ferrum-cli".to_string(),
2258        "--".to_string(),
2259        "replay-bundle".to_string(),
2260        bundle_dir.to_string_lossy().to_string(),
2261        "--out".to_string(),
2262        bundle_dir
2263            .join("engine_replay")
2264            .to_string_lossy()
2265            .to_string(),
2266        "--json".to_string(),
2267    ]
2268}
2269
2270fn shell_command(argv: &[String]) -> String {
2271    argv.iter()
2272        .map(|part| shell_quote(part))
2273        .collect::<Vec<_>>()
2274        .join(" ")
2275}
2276
2277fn shell_quote(value: &str) -> String {
2278    if value
2279        .chars()
2280        .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '/' | '.' | '_' | '-' | ':' | '@'))
2281    {
2282        value.to_string()
2283    } else {
2284        format!("'{}'", value.replace('\'', "'\\''"))
2285    }
2286}
2287
2288fn write_json_value(path: &Path, value: &serde_json::Value) -> std::result::Result<(), String> {
2289    let bytes = serde_json::to_vec_pretty(value).map_err(|err| err.to_string())?;
2290    fs::write(path, [bytes, b"\n".to_vec()].concat()).map_err(|err| err.to_string())
2291}
2292
2293fn bad_output_scan_json(
2294    request_id: &str,
2295    text: &str,
2296    failure_kind: Option<&str>,
2297    output_artifact_bytes: &[u8],
2298) -> serde_json::Value {
2299    let mut reasons = Vec::new();
2300    let mut first_span: Option<serde_json::Value> = None;
2301    for (needle, reason) in [
2302        ("<unk>", "reserved_token"),
2303        ("[PAD", "reserved_token"),
2304        ("<pad>", "reserved_token"),
2305        ("<|endoftext|>", "reserved_token"),
2306        ("<|im_start|>", "reserved_token"),
2307        ("<|im_end|>", "reserved_token"),
2308        ("<|reserved_special_token", "reserved_token"),
2309        ("\u{fffd}", "invalid_utf8"),
2310    ] {
2311        if let Some(index) = text.find(needle) {
2312            reasons.push(reason);
2313            first_span.get_or_insert_with(|| {
2314                serde_json::json!({
2315                    "byte_start": index,
2316                    "byte_end": index + needle.len(),
2317                    "text": needle,
2318                    "reason": reason
2319                })
2320            });
2321        }
2322    }
2323    if let Some(index) = first_mojibake_index(text) {
2324        reasons.push("mojibake");
2325        first_span.get_or_insert_with(|| {
2326            serde_json::json!({
2327                "byte_start": index,
2328                "byte_end": index + 1,
2329                "reason": "mojibake"
2330            })
2331        });
2332    }
2333    reasons.sort_unstable();
2334    reasons.dedup();
2335    let bad_output = !reasons.is_empty();
2336    serde_json::json!({
2337        "schema_version": OBSERVABILITY_PROFILE_SCHEMA_VERSION,
2338        "request_id": request_id,
2339        "bad_output": bad_output,
2340        "bad_text_count": if bad_output { 1 } else { 0 },
2341        "reasons": reasons,
2342        "first_bad_text_span": first_span,
2343        "failure_kind": failure_kind,
2344        "output_chars": text.chars().count(),
2345        "classified_output_sha256": sha256_hex(text.as_bytes()),
2346        "output_sha256": sha256_hex(output_artifact_bytes)
2347    })
2348}
2349
2350fn first_mojibake_index(text: &str) -> Option<usize> {
2351    let mut chars = text.char_indices().peekable();
2352    while let Some((index, ch)) = chars.next() {
2353        match ch {
2354            '\u{00c2}' | '\u{00c3}' => {
2355                if chars.peek().is_some_and(|(_, next)| !next.is_ascii()) {
2356                    return Some(index);
2357                }
2358            }
2359            '\u{00e2}' => {
2360                if chars.peek().is_some_and(|(_, next)| *next == '\u{20ac}') {
2361                    return Some(index);
2362                }
2363            }
2364            _ => {}
2365        }
2366    }
2367    None
2368}
2369
2370fn sha256_hex(bytes: &[u8]) -> String {
2371    let mut hasher = Sha256::new();
2372    hasher.update(bytes);
2373    format!("{:x}", hasher.finalize())
2374}
2375
2376struct ParsedChatModelOutput {
2377    visible: ParsedReasoningResponse,
2378    harmony_response: Option<ferrum_types::ApiChatResponse>,
2379}
2380
2381fn parse_chat_model_output(
2382    protocol: ModelOutputProtocol,
2383    text: &str,
2384    started_in_think: bool,
2385    finish_reason: FinishReason,
2386) -> std::result::Result<ParsedChatModelOutput, ServerError> {
2387    match protocol {
2388        ModelOutputProtocol::Text | ModelOutputProtocol::GemmaThought => {
2389            Ok(ParsedChatModelOutput {
2390                visible: parse_model_reasoning_response(protocol, text, started_in_think)
2391                    .map_err(|error| ServerError::InternalError(error.to_string()))?,
2392                harmony_response: None,
2393            })
2394        }
2395        ModelOutputProtocol::HarmonyGptOss => {
2396            let parsed = parse_harmony_response_for_finish_reason(text, Some(finish_reason))
2397                .map_err(|error| {
2398                    ServerError::InternalError(format!(
2399                        "model output did not satisfy the GPT-OSS Harmony protocol: {error}"
2400                    ))
2401                })?;
2402            let harmony_response =
2403                parsed
2404                    .tool_call
2405                    .map(|tool_call| ferrum_types::ApiChatResponse {
2406                        message: ferrum_types::ApiChatMessage {
2407                            role: ferrum_types::ApiMessageRole::Assistant,
2408                            content: String::new(),
2409                            name: None,
2410                            tool_calls: vec![ferrum_types::ApiToolCall {
2411                                id: format!("call_{}", Uuid::new_v4().simple()),
2412                                tool_type: "function".to_string(),
2413                                function: ferrum_types::ApiFunctionCall {
2414                                    name: tool_call.name,
2415                                    arguments: tool_call.arguments_json,
2416                                },
2417                            }],
2418                            tool_call_id: None,
2419                            function_call: None,
2420                        },
2421                        finish_reason: Some("tool_calls".to_string()),
2422                    });
2423            Ok(ParsedChatModelOutput {
2424                visible: ParsedReasoningResponse {
2425                    content: parsed.content,
2426                    reasoning: parsed.reasoning_content,
2427                },
2428                harmony_response,
2429            })
2430        }
2431    }
2432}
2433
2434/// Handle streaming chat completions
2435async fn handle_chat_completions_stream(
2436    state: AppState,
2437    openai_request: ChatCompletionsRequest,
2438    inference_request: InferenceRequest,
2439    benchmark_correlation: Option<BenchmarkRequestCorrelation>,
2440) -> std::result::Result<Response, ServerError> {
2441    let (tx, rx) = mpsc::unbounded_channel::<std::result::Result<Event, axum::Error>>();
2442
2443    // Spawn task to generate tokens
2444    let engine = state.llm.clone().ok_or_else(|| {
2445        ServerError::ServiceUnavailable("LLM engine not loaded; chat unavailable".into())
2446    })?;
2447    let request_id = Uuid::new_v4().to_string();
2448    let include_stream_usage = openai_request
2449        .stream_options
2450        .as_ref()
2451        .and_then(|opts| opts.include_usage)
2452        .unwrap_or(false);
2453    let output_contract = EffectiveChatOutputContract::resolve(&openai_request);
2454    let buffer_json_object_stream = matches!(
2455        output_contract,
2456        EffectiveChatOutputContract::JsonObjectContent
2457    );
2458    let buffer_strict_json_schema_stream = matches!(
2459        output_contract,
2460        EffectiveChatOutputContract::StrictJsonSchemaContent
2461    );
2462    let stream_api_request = match inference_request.api_request.as_ref() {
2463        Some(ferrum_types::ApiRequest::Chat(request)) => request.clone(),
2464        _ => api_chat_request(
2465            &openai_request,
2466            openai_request.tool_choice.as_ref(),
2467            ferrum_types::ApiToolCallProtocol::default(),
2468        ),
2469    };
2470    let buffer_structured_api_stream =
2471        ferrum_types::chat_api_may_emit_tool_or_function_call(&stream_api_request);
2472    let model_output_protocol = inference_request.sampling_params.model_output_protocol;
2473    let buffer_stream_output = buffer_json_object_stream
2474        || buffer_strict_json_schema_stream
2475        || buffer_structured_api_stream
2476        || model_output_protocol == ModelOutputProtocol::HarmonyGptOss;
2477    // R1-distill-style templates open the think block inside the prompt;
2478    // the parser must know generation starts mid-think.
2479    let started_in_think =
2480        has_unclosed_model_reasoning_block(model_output_protocol, &inference_request.prompt);
2481    let replay_request_id = inference_request.id.to_string();
2482    let profile_request_model = openai_request.model.clone();
2483    let profile_started_at = Instant::now();
2484    let request_memory_before = request_memory_sample_before(&state);
2485    let mut stream = match engine.infer_stream(inference_request).await {
2486        Ok(stream) => stream,
2487        Err(e) => {
2488            let failure_kind = e.observability_failure_kind();
2489            let error_kind = e.observability_error_kind();
2490            let error_message = e.to_string();
2491            if let Err(err) = write_chat_request_profile_event(
2492                &state,
2493                &replay_request_id,
2494                benchmark_correlation.as_ref(),
2495                &profile_request_model,
2496                true,
2497                "chat_completions_stream_start",
2498                profile_started_at,
2499                ChatRequestProfileTiming::default(),
2500                0,
2501                None,
2502                Some("error"),
2503                Some(ProfileError {
2504                    kind: error_kind.to_string(),
2505                    message: error_message.clone(),
2506                    blocking: false,
2507                }),
2508            ) {
2509                warn!("failed to write chat stream failure profile event: {}", err);
2510            }
2511            let engine_status = if chat_resource_failure_kind(failure_kind) {
2512                Some(engine.status().await)
2513            } else {
2514                None
2515            };
2516            error!(
2517                "Stream generation failed before first chunk: {}",
2518                error_message
2519            );
2520            if let Err(err) = write_chat_request_failure_diagnostics(
2521                &state,
2522                &replay_request_id,
2523                failure_kind,
2524                "chat_completions_stream_start",
2525                error_kind,
2526                &error_message,
2527                engine_status.as_ref(),
2528            ) {
2529                warn!("failed to write chat stream failure diagnostics: {}", err);
2530            }
2531            return Err(server_error_from_ferrum_error(e));
2532        }
2533    };
2534    let request_dump_dir = state.request_dump_dir.clone();
2535    let admission_summary = state
2536        .auto_config
2537        .as_ref()
2538        .map(|config| config.admission_summary_document());
2539    let diagnostics_engine = engine.clone();
2540    let profile_state = state.clone();
2541
2542    tokio::spawn(async move {
2543        let mut current_text = String::new();
2544        let mut output_token_ids = Vec::new();
2545        let mut first_engine_chunk_received_us = None;
2546        let mut first_sse_enqueue_us = None;
2547        let mut sent_reasoning_len = 0usize;
2548        let mut sent_content_len = 0usize;
2549
2550        loop {
2551            let next = tokio::select! {
2552                biased;
2553                _ = tx.closed() => break,
2554                next = stream.next() => next,
2555            };
2556            let Some(result) = next else {
2557                break;
2558            };
2559            match result {
2560                Ok(chunk) => {
2561                    if first_engine_chunk_received_us.is_none()
2562                        && (chunk.token.is_some() || !chunk.text.is_empty())
2563                    {
2564                        first_engine_chunk_received_us = Some(elapsed_us_since(profile_started_at));
2565                    }
2566                    if let Some(token) = chunk.token {
2567                        output_token_ids.push(token);
2568                    }
2569                    if !chunk.text.is_empty() {
2570                        current_text.push_str(&chunk.text);
2571
2572                        if !buffer_stream_output
2573                            && !should_defer_model_reasoning_stream_delta(
2574                                model_output_protocol,
2575                                &current_text,
2576                            )
2577                        {
2578                            let parsed = match parse_model_reasoning_response(
2579                                model_output_protocol,
2580                                &current_text,
2581                                started_in_think,
2582                            ) {
2583                                Ok(parsed) => parsed,
2584                                Err(error) => {
2585                                    let _ = tx.send(Ok(openai_error_sse_event(
2586                                        error.to_string(),
2587                                        "internal_server_error",
2588                                        Some("model_output"),
2589                                    )));
2590                                    let _ = tx.send(Ok(Event::default().data("[DONE]")));
2591                                    break;
2592                                }
2593                            };
2594                            let full_reasoning = parsed.reasoning.as_deref().unwrap_or("");
2595                            let reasoning_delta =
2596                                stream_text_delta(full_reasoning, &mut sent_reasoning_len);
2597                            let content_delta =
2598                                stream_text_delta(&parsed.content, &mut sent_content_len);
2599                            if !reasoning_delta.is_empty() || !content_delta.is_empty() {
2600                                // Create streaming response chunk
2601                                let response_chunk = ChatCompletionsResponse {
2602                                    id: request_id.clone(),
2603                                    object: "chat.completion.chunk".to_string(),
2604                                    created: chrono::Utc::now().timestamp() as u64,
2605                                    model: openai_request.model.clone(),
2606                                    choices: vec![ChatChoice {
2607                                        index: 0,
2608                                        message: None,
2609                                        delta: Some(ChatMessage {
2610                                            role: MessageRole::Assistant,
2611                                            content: content_delta,
2612                                            reasoning: (!reasoning_delta.is_empty())
2613                                                .then_some(reasoning_delta),
2614                                            name: None,
2615                                            tool_calls: None,
2616                                            tool_call_id: None,
2617                                            function_call: None,
2618                                        }),
2619                                        finish_reason: None,
2620                                    }],
2621                                    usage: None,
2622                                };
2623
2624                                let sse_event = Event::default()
2625                                    .json_data(&response_chunk)
2626                                    .unwrap_or_else(|_| Event::default().data("error"));
2627                                if tx.send(Ok(sse_event)).is_err() {
2628                                    break;
2629                                }
2630                                first_sse_enqueue_us
2631                                    .get_or_insert_with(|| elapsed_us_since(profile_started_at));
2632                            }
2633                        }
2634                    }
2635
2636                    if chunk.finish_reason.is_some() {
2637                        let terminal_finish_reason = chunk
2638                            .finish_reason
2639                            .expect("finish_reason presence checked above");
2640                        if let Err(err) = write_chat_prompt_token_evidence(
2641                            request_dump_dir.as_ref().map(|root| root.as_path()),
2642                            &replay_request_id,
2643                            &profile_request_model,
2644                            chunk.execution_evidence.as_ref(),
2645                        ) {
2646                            warn!("failed to write chat stream prompt-token evidence: {}", err);
2647                        }
2648                        let usage = chunk.usage.as_ref().map(openai_usage_from_token_usage);
2649                        let parsed_model_output = match parse_chat_model_output(
2650                            model_output_protocol,
2651                            &current_text,
2652                            started_in_think,
2653                            terminal_finish_reason,
2654                        ) {
2655                            Ok(parsed) => parsed,
2656                            Err(error) => {
2657                                let error_event = openai_error_sse_event(
2658                                    stream_validation_error_message(error),
2659                                    "internal_server_error",
2660                                    Some("model_output"),
2661                                );
2662                                let _ = tx.send(Ok(error_event));
2663                                let _ = tx.send(Ok(Event::default().data("[DONE]")));
2664                                break;
2665                            }
2666                        };
2667                        let mut parsed_final = parsed_model_output.visible;
2668                        parsed_final.content = normalize_structured_response_content(
2669                            &openai_request,
2670                            &parsed_final.content,
2671                        );
2672                        let structured_chat_response =
2673                            finish_reason_allows_structured_api_response(terminal_finish_reason)
2674                                .then(|| match chunk.api_response.as_ref() {
2675                                    // The native envelope owns the recipient and whether this is
2676                                    // a call at all, including a final answer containing JSON.
2677                                    _ if model_output_protocol
2678                                        == ModelOutputProtocol::HarmonyGptOss =>
2679                                    {
2680                                        parsed_model_output.harmony_response.clone()
2681                                    }
2682                                    Some(ferrum_types::ApiResponse::Chat(response)) => {
2683                                        Some(response.clone())
2684                                    }
2685                                    _ if buffer_structured_api_stream => {
2686                                        chat_api_response_from_parsed_generated_text(
2687                                            &stream_api_request,
2688                                            &parsed_final,
2689                                            terminal_finish_reason,
2690                                        )
2691                                    }
2692                                    _ => None,
2693                                })
2694                                .flatten();
2695
2696                        if let Some(chat_response) = structured_chat_response.as_ref() {
2697                            if let Err(e) =
2698                                validate_structured_tool_response(&openai_request, chat_response)
2699                            {
2700                                let error_event = openai_error_sse_event(
2701                                    stream_validation_error_message(e),
2702                                    "internal_server_error",
2703                                    Some("tool_choice"),
2704                                );
2705                                let _ = tx.send(Ok(error_event));
2706                                let _ = tx.send(Ok(Event::default().data("[DONE]")));
2707                                break;
2708                            }
2709                        } else if tool_choice_required(&openai_request) {
2710                            log_required_tool_choice_failure(
2711                                &openai_request,
2712                                &parsed_final.content,
2713                                parsed_final.reasoning.as_deref(),
2714                            );
2715                            let error_event = openai_error_sse_event(
2716                                "model output did not satisfy required tool_choice",
2717                                "invalid_request_error",
2718                                Some("tool_choice"),
2719                            );
2720                            let _ = tx.send(Ok(error_event));
2721                            let _ = tx.send(Ok(Event::default().data("[DONE]")));
2722                            break;
2723                        }
2724                        if let Err(e) = validate_hard_structured_response(
2725                            &openai_request,
2726                            &parsed_final.content,
2727                        ) {
2728                            let error_event = openai_error_sse_event(
2729                                stream_validation_error_message(e),
2730                                "internal_server_error",
2731                                structured_response_error_param(output_contract),
2732                            );
2733                            let _ = tx.send(Ok(error_event));
2734                            let _ = tx.send(Ok(Event::default().data("[DONE]")));
2735                            break;
2736                        }
2737
2738                        if let Some(chat_response) = structured_chat_response.as_ref() {
2739                            let mut delta = openai_chat_delta_from_api(&chat_response.message);
2740                            if delta.reasoning.is_none() {
2741                                delta.reasoning = parsed_final.reasoning.clone();
2742                            }
2743                            let response_chunk = ChatCompletionsResponse {
2744                                id: request_id.clone(),
2745                                object: "chat.completion.chunk".to_string(),
2746                                created: chrono::Utc::now().timestamp() as u64,
2747                                model: openai_request.model.clone(),
2748                                choices: vec![ChatChoice {
2749                                    index: 0,
2750                                    message: None,
2751                                    delta: Some(delta),
2752                                    finish_reason: None,
2753                                }],
2754                                usage: None,
2755                            };
2756
2757                            let sse_event = Event::default()
2758                                .json_data(&response_chunk)
2759                                .unwrap_or_else(|_| Event::default().data("error"));
2760                            if tx.send(Ok(sse_event)).is_err() {
2761                                break;
2762                            }
2763                            first_sse_enqueue_us
2764                                .get_or_insert_with(|| elapsed_us_since(profile_started_at));
2765                        } else if buffer_structured_api_stream
2766                            && parsed_final.content.trim().is_empty()
2767                        {
2768                            let error_event = openai_error_sse_event(
2769                                "model output did not satisfy tool/function call request",
2770                                "internal_server_error",
2771                                Some("tool_choice"),
2772                            );
2773                            let _ = tx.send(Ok(error_event));
2774                            let _ = tx.send(Ok(Event::default().data("[DONE]")));
2775                            break;
2776                        } else if !current_text.is_empty() {
2777                            // Flush safe text held with an incomplete trailing
2778                            // marker, including when text and finish share a chunk.
2779                            // Buffered streams have sent lengths of zero.
2780                            let content_delta =
2781                                stream_text_delta(&parsed_final.content, &mut sent_content_len);
2782                            let reasoning_delta = stream_text_delta(
2783                                parsed_final.reasoning.as_deref().unwrap_or(""),
2784                                &mut sent_reasoning_len,
2785                            );
2786                            if !content_delta.is_empty() || !reasoning_delta.is_empty() {
2787                                let response_chunk = ChatCompletionsResponse {
2788                                    id: request_id.clone(),
2789                                    object: "chat.completion.chunk".to_string(),
2790                                    created: chrono::Utc::now().timestamp() as u64,
2791                                    model: openai_request.model.clone(),
2792                                    choices: vec![ChatChoice {
2793                                        index: 0,
2794                                        message: None,
2795                                        delta: Some(ChatMessage {
2796                                            role: MessageRole::Assistant,
2797                                            content: content_delta,
2798                                            reasoning: (!reasoning_delta.is_empty())
2799                                                .then_some(reasoning_delta),
2800                                            name: None,
2801                                            tool_calls: None,
2802                                            tool_call_id: None,
2803                                            function_call: None,
2804                                        }),
2805                                        finish_reason: None,
2806                                    }],
2807                                    usage: None,
2808                                };
2809
2810                                let sse_event = Event::default()
2811                                    .json_data(&response_chunk)
2812                                    .unwrap_or_else(|_| Event::default().data("error"));
2813                                if tx.send(Ok(sse_event)).is_err() {
2814                                    break;
2815                                }
2816                                first_sse_enqueue_us
2817                                    .get_or_insert_with(|| elapsed_us_since(profile_started_at));
2818                            }
2819                        }
2820                        // Send final chunk. OpenAI-style streaming
2821                        // clients (e.g. `vllm bench serve`) blindly
2822                        // read `choices[0]["delta"]` on every chunk
2823                        // that has any `choices` entries, so the
2824                        // last chunk must include `delta` even when
2825                        // empty. Skipping it triggers
2826                        // `KeyError: 'delta'` on the client side
2827                        // and the request is reported as failed
2828                        // despite returning a 200 with content.
2829                        let final_finish_reason = structured_chat_response
2830                            .as_ref()
2831                            .and_then(|response| response.finish_reason.clone())
2832                            .or_else(|| chunk.finish_reason.as_ref().map(finish_reason_to_string))
2833                            .or(Some("length".to_string()));
2834                        let final_chunk = ChatCompletionsResponse {
2835                            id: request_id.clone(),
2836                            object: "chat.completion.chunk".to_string(),
2837                            created: chrono::Utc::now().timestamp() as u64,
2838                            model: openai_request.model.clone(),
2839                            choices: vec![ChatChoice {
2840                                index: 0,
2841                                message: None,
2842                                delta: Some(ChatMessage {
2843                                    role: MessageRole::Assistant,
2844                                    content: String::new(),
2845                                    reasoning: None,
2846                                    name: None,
2847                                    tool_calls: None,
2848                                    tool_call_id: None,
2849                                    function_call: None,
2850                                }),
2851                                finish_reason: final_finish_reason.clone(),
2852                            }],
2853                            usage: None,
2854                        };
2855
2856                        let final_event = Event::default()
2857                            .json_data(&final_chunk)
2858                            .unwrap_or_else(|_| Event::default().data("error"));
2859                        if tx.send(Ok(final_event)).is_ok() {
2860                            first_sse_enqueue_us
2861                                .get_or_insert_with(|| elapsed_us_since(profile_started_at));
2862                        }
2863                        let completion_token_count = chunk
2864                            .usage
2865                            .as_ref()
2866                            .map(|usage| usage.completion_tokens)
2867                            .unwrap_or(output_token_ids.len());
2868                        let replay_output_token_ids = chunk
2869                            .execution_evidence
2870                            .as_ref()
2871                            .map(|evidence| evidence.output_token_ids.as_slice())
2872                            .filter(|tokens| tokens.len() == completion_token_count)
2873                            .unwrap_or(output_token_ids.as_slice());
2874                        if let Err(err) = write_chat_request_completion_replay_bundle(
2875                            request_dump_dir.as_ref().map(|root| root.as_path()),
2876                            &replay_request_id,
2877                            &parsed_final.content,
2878                            replay_output_token_ids,
2879                            final_finish_reason.as_deref(),
2880                        ) {
2881                            warn!("failed to write chat stream replay bundle: {}", err);
2882                        }
2883                        if let Err(err) = write_chat_request_profile_event(
2884                            &profile_state,
2885                            &replay_request_id,
2886                            benchmark_correlation.as_ref(),
2887                            &profile_request_model,
2888                            true,
2889                            "chat_completions_stream_complete",
2890                            profile_started_at,
2891                            ChatRequestProfileTiming {
2892                                engine_evidence: chunk.execution_evidence.as_ref(),
2893                                first_engine_chunk_received_us,
2894                                first_sse_enqueue_us,
2895                            },
2896                            completion_token_count,
2897                            chunk.usage.as_ref(),
2898                            final_finish_reason.as_deref(),
2899                            None,
2900                        ) {
2901                            warn!("failed to write chat stream profile event: {}", err);
2902                        }
2903                        if let Err(err) = maybe_write_first_request_memory_stage(
2904                            &profile_state,
2905                            &replay_request_id,
2906                            benchmark_correlation.as_ref(),
2907                            &profile_request_model,
2908                            true,
2909                            profile_started_at,
2910                            request_memory_before,
2911                        ) {
2912                            warn!("failed to write chat stream memory profile event: {}", err);
2913                        }
2914                        if include_stream_usage && usage.is_some() {
2915                            let usage_chunk = ChatCompletionsResponse {
2916                                id: request_id.clone(),
2917                                object: "chat.completion.chunk".to_string(),
2918                                created: chrono::Utc::now().timestamp() as u64,
2919                                model: openai_request.model.clone(),
2920                                choices: vec![],
2921                                usage,
2922                            };
2923                            let usage_event = Event::default()
2924                                .json_data(&usage_chunk)
2925                                .unwrap_or_else(|_| Event::default().data("error"));
2926                            let _ = tx.send(Ok(usage_event));
2927                        }
2928                        let _ = tx.send(Ok(Event::default().data("[DONE]")));
2929                        break;
2930                    }
2931                }
2932                Err(e) => {
2933                    let failure_kind = e.observability_failure_kind();
2934                    let error_kind = e.observability_error_kind();
2935                    let error_message = e.to_string();
2936                    let engine_status = if chat_resource_failure_kind(failure_kind) {
2937                        Some(diagnostics_engine.status().await)
2938                    } else {
2939                        None
2940                    };
2941                    error!("Stream generation error: {}", error_message);
2942                    if let Err(err) = write_chat_request_profile_event(
2943                        &profile_state,
2944                        &replay_request_id,
2945                        benchmark_correlation.as_ref(),
2946                        &profile_request_model,
2947                        true,
2948                        "chat_completions_stream_next",
2949                        profile_started_at,
2950                        ChatRequestProfileTiming {
2951                            engine_evidence: None,
2952                            first_engine_chunk_received_us,
2953                            first_sse_enqueue_us,
2954                        },
2955                        output_token_ids.len(),
2956                        None,
2957                        Some("error"),
2958                        Some(ProfileError {
2959                            kind: error_kind.to_string(),
2960                            message: error_message.clone(),
2961                            blocking: false,
2962                        }),
2963                    ) {
2964                        warn!("failed to write chat stream chunk profile event: {}", err);
2965                    }
2966                    if let Err(err) = write_chat_request_failure_diagnostics_at_root(
2967                        request_dump_dir.as_ref().map(|root| root.as_path()),
2968                        admission_summary.as_ref(),
2969                        engine_status.as_ref(),
2970                        &replay_request_id,
2971                        failure_kind,
2972                        "chat_completions_stream_next",
2973                        error_kind,
2974                        &error_message,
2975                    ) {
2976                        warn!(
2977                            "failed to write chat stream chunk failure diagnostics: {}",
2978                            err
2979                        );
2980                    }
2981                    let _ = tx.send(Ok(openai_error_sse_event(
2982                        error_message,
2983                        "internal_server_error",
2984                        None,
2985                    )));
2986                    let _ = tx.send(Ok(Event::default().data("[DONE]")));
2987                    break;
2988                }
2989            }
2990        }
2991    });
2992
2993    let stream = tokio_stream::wrappers::UnboundedReceiverStream::new(rx);
2994    let sse_stream = Sse::new(stream);
2995
2996    Ok(sse_stream.into_response())
2997}
2998
2999/// Handle non-streaming chat completions
3000async fn handle_chat_completions_sync(
3001    state: AppState,
3002    openai_request: ChatCompletionsRequest,
3003    inference_request: InferenceRequest,
3004    session_context: Option<SessionContext>,
3005    benchmark_correlation: Option<BenchmarkRequestCorrelation>,
3006) -> std::result::Result<Response, ServerError> {
3007    info!("Processing non-streaming chat completion");
3008
3009    let engine = state.llm.clone().ok_or_else(|| {
3010        ServerError::ServiceUnavailable("LLM engine not loaded; chat unavailable".into())
3011    })?;
3012    let request_chat_api = inference_request
3013        .api_request
3014        .as_ref()
3015        .and_then(|api_request| match api_request {
3016            ferrum_types::ApiRequest::Chat(chat_request) => {
3017                ferrum_types::chat_api_may_emit_tool_or_function_call(chat_request)
3018                    .then(|| chat_request.clone())
3019            }
3020            _ => None,
3021        });
3022    let model_output_protocol = inference_request.sampling_params.model_output_protocol;
3023    // R1-distill-style templates open the think block inside the prompt.
3024    let started_in_think =
3025        has_unclosed_model_reasoning_block(model_output_protocol, &inference_request.prompt);
3026    let replay_request_id = inference_request.id.to_string();
3027    let profile_request_model = openai_request.model.clone();
3028    let profile_started_at = Instant::now();
3029    let request_memory_before = request_memory_sample_before(&state);
3030    match engine.infer(inference_request).await {
3031        Ok(output) => {
3032            let InferenceResponse {
3033                text: output_text,
3034                tokens,
3035                finish_reason,
3036                usage,
3037                api_response,
3038                execution_evidence,
3039                ..
3040            } = output;
3041            if let Err(err) = write_chat_prompt_token_evidence(
3042                state.request_dump_dir.as_ref().map(|root| root.as_path()),
3043                &replay_request_id,
3044                &profile_request_model,
3045                execution_evidence.as_ref(),
3046            ) {
3047                warn!("failed to write chat prompt-token evidence: {}", err);
3048            }
3049
3050            // OpenAI stop strings mark a boundary and are not included in the
3051            // returned completion. Hard structured formats are never repaired
3052            // here; malformed output must fail the response contract below.
3053            let stop_sequences = openai_request.stop.clone().unwrap_or_default();
3054            let content = strip_after_stop(&output_text, &stop_sequences);
3055            let parsed_model_output = parse_chat_model_output(
3056                model_output_protocol,
3057                &content,
3058                started_in_think,
3059                finish_reason,
3060            )?;
3061            let parsed = parsed_model_output.visible;
3062            let visible_content =
3063                normalize_structured_response_content(&openai_request, &parsed.content);
3064            let mut message = ChatMessage {
3065                role: MessageRole::Assistant,
3066                content: visible_content,
3067                reasoning: parsed.reasoning.clone(),
3068                name: None,
3069                tool_calls: None,
3070                tool_call_id: None,
3071                function_call: None,
3072            };
3073            let mut openai_finish_reason = finish_reason_to_string(&finish_reason);
3074            let structured_chat_response =
3075                finish_reason_allows_structured_api_response(finish_reason)
3076                    .then(|| match api_response.as_ref() {
3077                        // The native envelope owns the recipient and whether this is
3078                        // a call at all, including a final answer containing JSON.
3079                        _ if model_output_protocol == ModelOutputProtocol::HarmonyGptOss => {
3080                            parsed_model_output.harmony_response.clone()
3081                        }
3082                        Some(ferrum_types::ApiResponse::Chat(chat_response)) => {
3083                            Some(chat_response.clone())
3084                        }
3085                        _ => match request_chat_api.as_ref() {
3086                            Some(chat_request) => chat_api_response_from_parsed_generated_text(
3087                                chat_request,
3088                                &parsed,
3089                                finish_reason,
3090                            ),
3091                            _ => None,
3092                        },
3093                    })
3094                    .flatten();
3095            if let Some(chat_response) = structured_chat_response.as_ref() {
3096                if let Err(error) =
3097                    validate_structured_tool_response(&openai_request, chat_response)
3098                {
3099                    if let Err(err) = write_chat_request_profile_event(
3100                        &state,
3101                        &replay_request_id,
3102                        benchmark_correlation.as_ref(),
3103                        &profile_request_model,
3104                        false,
3105                        "chat_completions_sync_tool_contract",
3106                        profile_started_at,
3107                        ChatRequestProfileTiming {
3108                            engine_evidence: execution_evidence.as_ref(),
3109                            ..Default::default()
3110                        },
3111                        tokens.len(),
3112                        Some(&usage),
3113                        Some("error"),
3114                        Some(ProfileError {
3115                            kind: "tool_contract_failure".to_string(),
3116                            message: format!("{error:?}"),
3117                            blocking: true,
3118                        }),
3119                    ) {
3120                        warn!("failed to write chat tool-contract profile event: {}", err);
3121                    }
3122                    return Err(error);
3123                }
3124                message = openai_chat_message_from_api(&chat_response.message);
3125                if message.reasoning.is_none() {
3126                    message.reasoning = parsed.reasoning.clone();
3127                }
3128                if let Some(reason) = &chat_response.finish_reason {
3129                    openai_finish_reason = reason.clone();
3130                }
3131            } else if tool_choice_required(&openai_request) {
3132                log_required_tool_choice_failure(
3133                    &openai_request,
3134                    &parsed.content,
3135                    parsed.reasoning.as_deref(),
3136                );
3137                if let Err(err) = write_chat_request_profile_event(
3138                    &state,
3139                    &replay_request_id,
3140                    benchmark_correlation.as_ref(),
3141                    &profile_request_model,
3142                    false,
3143                    "chat_completions_sync_tool_choice",
3144                    profile_started_at,
3145                    ChatRequestProfileTiming {
3146                        engine_evidence: execution_evidence.as_ref(),
3147                        ..Default::default()
3148                    },
3149                    tokens.len(),
3150                    Some(&usage),
3151                    Some("error"),
3152                    Some(ProfileError {
3153                        kind: "required_tool_failure".to_string(),
3154                        message: "model output did not satisfy required tool_choice".to_string(),
3155                        blocking: true,
3156                    }),
3157                ) {
3158                    warn!("failed to write chat tool-choice profile event: {}", err);
3159                }
3160                return Err(ServerError::invalid_request(
3161                    "model output did not satisfy required tool_choice",
3162                    Some("tool_choice"),
3163                ));
3164            }
3165            if let Err(error) = validate_hard_structured_response(&openai_request, &message.content)
3166            {
3167                if let Err(err) = write_chat_request_profile_event(
3168                    &state,
3169                    &replay_request_id,
3170                    benchmark_correlation.as_ref(),
3171                    &profile_request_model,
3172                    false,
3173                    "chat_completions_sync_structured_output",
3174                    profile_started_at,
3175                    ChatRequestProfileTiming {
3176                        engine_evidence: execution_evidence.as_ref(),
3177                        ..Default::default()
3178                    },
3179                    tokens.len(),
3180                    Some(&usage),
3181                    Some("error"),
3182                    Some(ProfileError {
3183                        kind: "structured_output_failure".to_string(),
3184                        message: format!("{error:?}"),
3185                        blocking: true,
3186                    }),
3187                ) {
3188                    warn!("failed to write chat strict-schema profile event: {}", err);
3189                }
3190                return Err(error);
3191            }
3192            if let Err(err) = write_chat_request_completion_replay_bundle(
3193                state.request_dump_dir.as_ref().map(|root| root.as_path()),
3194                &replay_request_id,
3195                &message.content,
3196                &tokens,
3197                Some(&openai_finish_reason),
3198            ) {
3199                warn!("failed to write chat completion replay bundle: {}", err);
3200            }
3201            if let Err(err) = write_chat_request_profile_event(
3202                &state,
3203                &replay_request_id,
3204                benchmark_correlation.as_ref(),
3205                &profile_request_model,
3206                false,
3207                "chat_completions_sync_complete",
3208                profile_started_at,
3209                ChatRequestProfileTiming {
3210                    engine_evidence: execution_evidence.as_ref(),
3211                    ..Default::default()
3212                },
3213                tokens.len(),
3214                Some(&usage),
3215                Some(&openai_finish_reason),
3216                None,
3217            ) {
3218                warn!("failed to write chat sync profile event: {}", err);
3219            }
3220            if let Err(err) = maybe_write_first_request_memory_stage(
3221                &state,
3222                &replay_request_id,
3223                benchmark_correlation.as_ref(),
3224                &profile_request_model,
3225                false,
3226                profile_started_at,
3227                request_memory_before,
3228            ) {
3229                warn!("failed to write chat sync memory profile event: {}", err);
3230            }
3231            state
3232                .cache
3233                .update_session(session_context, message.clone(), &CachePolicy::current());
3234            let response = ChatCompletionsResponse {
3235                id: Uuid::new_v4().to_string(),
3236                object: "chat.completion".to_string(),
3237                created: chrono::Utc::now().timestamp() as u64,
3238                model: openai_request.model,
3239                choices: vec![ChatChoice {
3240                    index: 0,
3241                    message: Some(message),
3242                    delta: None,
3243                    finish_reason: Some(openai_finish_reason),
3244                }],
3245                usage: Some(openai_usage_from_token_usage(&usage)),
3246            };
3247
3248            Ok(Json(response).into_response())
3249        }
3250        Err(e) => {
3251            let failure_kind = e.observability_failure_kind();
3252            let error_kind = e.observability_error_kind();
3253            let error_message = e.to_string();
3254            let engine_status = if chat_resource_failure_kind(failure_kind) {
3255                Some(engine.status().await)
3256            } else {
3257                None
3258            };
3259            error!("Generation failed: {}", error_message);
3260            if let Err(err) = write_chat_request_profile_event(
3261                &state,
3262                &replay_request_id,
3263                benchmark_correlation.as_ref(),
3264                &profile_request_model,
3265                false,
3266                "chat_completions_sync",
3267                profile_started_at,
3268                ChatRequestProfileTiming::default(),
3269                0,
3270                None,
3271                Some("error"),
3272                Some(ProfileError {
3273                    kind: error_kind.to_string(),
3274                    message: error_message.clone(),
3275                    blocking: false,
3276                }),
3277            ) {
3278                warn!("failed to write chat sync failure profile event: {}", err);
3279            }
3280            if let Err(err) = write_chat_request_failure_diagnostics(
3281                &state,
3282                &replay_request_id,
3283                failure_kind,
3284                "chat_completions_sync",
3285                error_kind,
3286                &error_message,
3287                engine_status.as_ref(),
3288            ) {
3289                warn!(
3290                    "failed to write chat generation failure diagnostics: {}",
3291                    err
3292                );
3293            }
3294            Err(server_error_from_ferrum_error(e))
3295        }
3296    }
3297}
3298
3299#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3300enum EffectiveChatOutputContract {
3301    RequiredToolCall,
3302    StrictJsonSchemaContent,
3303    JsonObjectContent,
3304    BestEffortJsonSchemaContent,
3305    Text,
3306}
3307
3308#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3309enum ChatOutputBudget {
3310    AutoCeiling(u32),
3311    Explicit(u32),
3312}
3313
3314impl ChatOutputBudget {
3315    fn resolve(request: &ChatCompletionsRequest) -> Self {
3316        request
3317            .max_completion_tokens
3318            .or(request.max_tokens)
3319            .map(Self::Explicit)
3320            .unwrap_or(Self::AutoCeiling(DEFAULT_COMPLETION_MAX_TOKENS))
3321    }
3322
3323    const fn ceiling(self) -> u32 {
3324        match self {
3325            Self::AutoCeiling(value) | Self::Explicit(value) => value,
3326        }
3327    }
3328
3329    const fn is_auto(self) -> bool {
3330        matches!(self, Self::AutoCeiling(_))
3331    }
3332}
3333
3334impl EffectiveChatOutputContract {
3335    fn resolve(request: &ChatCompletionsRequest) -> Self {
3336        if tool_choice_required(request) {
3337            return Self::RequiredToolCall;
3338        }
3339        let Some(format) = request.response_format.as_ref() else {
3340            return Self::Text;
3341        };
3342        match format.format_type.as_str() {
3343            "json_schema"
3344                if format
3345                    .json_schema
3346                    .as_ref()
3347                    .and_then(|schema| schema.strict)
3348                    .unwrap_or(false) =>
3349            {
3350                Self::StrictJsonSchemaContent
3351            }
3352            "json_schema" => Self::BestEffortJsonSchemaContent,
3353            "json_object" => Self::JsonObjectContent,
3354            _ => Self::Text,
3355        }
3356    }
3357
3358    fn accepts_requested_response_format(self) -> bool {
3359        !matches!(self, Self::RequiredToolCall)
3360    }
3361}
3362
3363/// Convert OpenAI chat request to internal inference request
3364#[allow(dead_code)]
3365fn convert_chat_request(
3366    request: &ChatCompletionsRequest,
3367) -> ferrum_types::Result<InferenceRequest> {
3368    convert_chat_request_with_template_model(request, &request.model, None)
3369}
3370
3371/// Convert OpenAI chat request to internal inference request.
3372///
3373/// `template_model_id` is the loaded model id used for prompt-template family
3374/// detection. The request `model` field may be an OpenAI-compatible alias such
3375/// as "ferrum"; using it for template selection can feed a fallback prompt to
3376/// a Qwen/Llama model.
3377fn convert_chat_request_with_template_model(
3378    request: &ChatCompletionsRequest,
3379    template_model_id: &str,
3380    model_template: Option<&ModelChatTemplate>,
3381) -> ferrum_types::Result<InferenceRequest> {
3382    convert_chat_request_with_template_model_and_default(
3383        request,
3384        template_model_id,
3385        model_template,
3386        None,
3387        true,
3388        None,
3389    )
3390}
3391
3392fn convert_chat_request_with_template_model_and_default(
3393    request: &ChatCompletionsRequest,
3394    template_model_id: &str,
3395    model_template: Option<&ModelChatTemplate>,
3396    default_enable_thinking: Option<bool>,
3397    interleaved_system_coalescing: bool,
3398    message_phases: Option<&[Option<AssistantMessagePhase>]>,
3399) -> ferrum_types::Result<InferenceRequest> {
3400    let no_tools: &[ChatTool] = &[];
3401    let tools = if tool_choice_none_hides_tools(request.tool_choice.as_ref(), model_template) {
3402        no_tools
3403    } else {
3404        request.tools.as_deref().unwrap_or_default()
3405    };
3406    let default_tool_choice =
3407        default_auto_tool_choice_for_tools(tools, request.tool_choice.as_ref());
3408    let effective_tool_choice = request
3409        .tool_choice
3410        .as_ref()
3411        .or(default_tool_choice.as_ref());
3412    let functions = request.functions.as_deref().unwrap_or_default();
3413    let model_output_protocol = model_template
3414        .map(|template| template.output_protocol)
3415        .unwrap_or(ModelOutputProtocol::Text);
3416    let output_contract = EffectiveChatOutputContract::resolve(request);
3417    let output_budget = ChatOutputBudget::resolve(request);
3418    // Harmony tool calls own their complete channel/message/call envelope.
3419    // Applying the generic tool-argument JSON grammar at token zero would
3420    // mask that envelope and force the model to emit bare arguments instead.
3421    let forced_response_format = (model_output_protocol != ModelOutputProtocol::HarmonyGptOss)
3422        .then(|| forced_tool_choice_response_format(request))
3423        .flatten();
3424    let hard_tool_call_contract = forced_response_format.is_some();
3425    let requested_response_format = output_contract
3426        .accepts_requested_response_format()
3427        .then(|| requested_response_format_for_sampling(request))
3428        .transpose()?
3429        .flatten();
3430    let chat_template_options =
3431        chat_template_options_for_request(request, model_template, default_enable_thinking)?;
3432    let response_format = forced_response_format
3433        .or(requested_response_format)
3434        .unwrap_or(ferrum_types::ResponseFormat::Text);
3435    let model_generated_thinking = model_template.is_some_and(|template| {
3436        template.reasoning_protocol == ModelReasoningProtocol::ModelGenerated
3437            && template.reasoning_enabled(chat_template_options.enable_thinking)
3438    });
3439    let reasoning_enabled = model_template
3440        .is_some_and(|template| template.reasoning_enabled(chat_template_options.enable_thinking));
3441    let (render_messages, render_message_phases) = render_messages_with_response_format_instruction(
3442        request,
3443        output_contract,
3444        reasoning_enabled,
3445        message_phases,
3446    );
3447    let prompt = if tools.is_empty() && functions.is_empty() {
3448        render_chat_prompt_with_model_template_options_and_compatibility(
3449            &render_messages,
3450            template_model_id,
3451            model_template,
3452            &chat_template_options,
3453            interleaved_system_coalescing,
3454            Some(&render_message_phases),
3455        )?
3456    } else {
3457        render_chat_prompt_with_tools_and_model_template_compatibility(
3458            &render_messages,
3459            template_model_id,
3460            model_template,
3461            &chat_template_options,
3462            tools,
3463            effective_tool_choice,
3464            functions,
3465            request.function_call.as_ref(),
3466            interleaved_system_coalescing,
3467            Some(&render_message_phases),
3468        )?
3469    };
3470    let tool_call_protocol = model_template
3471        .map(|template| template.tool_call_protocol)
3472        .unwrap_or_default();
3473    let api_chat = api_chat_request(request, effective_tool_choice, tool_call_protocol);
3474    let mut metadata = HashMap::new();
3475    metadata.insert(
3476        "openai_messages".to_string(),
3477        serde_json::to_value(&request.messages)?,
3478    );
3479    if let Some(tools) = &request.tools {
3480        metadata.insert("openai_tools".to_string(), serde_json::to_value(tools)?);
3481    }
3482    if let Some(tool_choice) = effective_tool_choice {
3483        metadata.insert(
3484            "openai_tool_choice".to_string(),
3485            serde_json::to_value(tool_choice)?,
3486        );
3487    }
3488    if let Some(functions) = &request.functions {
3489        metadata.insert(
3490            "openai_legacy_functions".to_string(),
3491            serde_json::to_value(functions)?,
3492        );
3493    }
3494    if let Some(function_call) = &request.function_call {
3495        metadata.insert(
3496            "openai_legacy_function_call".to_string(),
3497            serde_json::to_value(function_call)?,
3498        );
3499    }
3500    if request.ignore_eos.unwrap_or(false) {
3501        metadata.insert("ferrum_ignore_eos".to_string(), serde_json::json!(true));
3502    }
3503    if output_budget.is_auto() {
3504        metadata.insert(
3505            DEFAULT_MAX_TOKENS_METADATA_KEY.to_string(),
3506            serde_json::json!(true),
3507        );
3508    }
3509    let prompt_opened_thinking = has_unclosed_model_reasoning_block(model_output_protocol, &prompt);
3510    let reasoning_markers = model_reasoning_markers(model_output_protocol);
3511    if !prompt_opened_thinking {
3512        let mut forbidden = reasoning_markers
3513            .map(|(_, close)| vec![close.to_string()])
3514            .unwrap_or_default();
3515        if hard_tool_call_contract {
3516            for token_text in INITIAL_STRUCTURED_CALL_FORBIDDEN_TOKEN_TEXTS {
3517                push_unique_forbidden_token_text(&mut forbidden, token_text);
3518            }
3519            if let Some(eos) = model_template.as_ref().and_then(|template| {
3520                template
3521                    .eos_token
3522                    .as_deref()
3523                    .filter(|token| !token.is_empty())
3524            }) {
3525                push_unique_forbidden_token_text(&mut forbidden, eos);
3526            }
3527        }
3528        if model_output_protocol == ModelOutputProtocol::Text
3529            && chat_template_options.enable_thinking == Some(false)
3530        {
3531            push_unique_forbidden_token_text(&mut forbidden, THINK_START_TAG);
3532        }
3533        metadata.insert(
3534            INITIAL_FORBIDDEN_TOKEN_TEXTS_METADATA_KEY.to_string(),
3535            serde_json::json!(forbidden),
3536        );
3537    }
3538    let structured_output = !matches!(response_format, ferrum_types::ResponseFormat::Text);
3539    let structured_output_after_reasoning = structured_output
3540        && model_output_protocol == ModelOutputProtocol::Text
3541        && (prompt_opened_thinking || model_generated_thinking);
3542    let structured_output_start =
3543        if structured_output && model_output_protocol == ModelOutputProtocol::HarmonyGptOss {
3544            StructuredOutputStart::HarmonyFinal
3545        } else if structured_output && model_output_protocol == ModelOutputProtocol::GemmaThought {
3546            let (opening, closing) = reasoning_markers.expect("Gemma thought markers");
3547            if prompt_opened_thinking {
3548                StructuredOutputStart::AfterDelimiter(closing.to_string())
3549            } else if prompt.trim_end().ends_with(closing) {
3550                StructuredOutputStart::Immediate
3551            } else {
3552                StructuredOutputStart::AfterReasoningEnvelope {
3553                    opening: opening.to_string(),
3554                    closing: closing.to_string(),
3555                    allow_reasoning: reasoning_enabled,
3556                }
3557            }
3558        } else if structured_output_after_reasoning {
3559            StructuredOutputStart::AfterDelimiter(THINK_END_TAG.to_string())
3560        } else {
3561            StructuredOutputStart::Immediate
3562        };
3563    let delayed_grammar = matches!(
3564        structured_output_start,
3565        StructuredOutputStart::AfterDelimiter(_)
3566            | StructuredOutputStart::AfterReasoningEnvelope { .. }
3567    );
3568    let response_completion_boundary = if let Some((_, closing)) =
3569        reasoning_markers.filter(|_| prompt_opened_thinking || delayed_grammar)
3570    {
3571        ResponseCompletionBoundary::AfterDelimiterAndPayload {
3572            delimiter: closing.to_string(),
3573            alternate_envelope: api_chat.generated_response_envelope(),
3574        }
3575    } else {
3576        ResponseCompletionBoundary::Immediate
3577    };
3578
3579    Ok(InferenceRequest {
3580        id: RequestId(Uuid::new_v4()),
3581        model_id: ModelId(request.model.clone()),
3582        prompt,
3583        sampling_params: SamplingParams {
3584            max_tokens: output_budget.ceiling() as usize,
3585            temperature: request.temperature.unwrap_or(DEFAULT_SAMPLING_TEMPERATURE),
3586            top_p: request.top_p.unwrap_or(DEFAULT_SAMPLING_TOP_P),
3587            top_k: request
3588                .top_k
3589                .filter(|value| *value > 0)
3590                .and_then(|value| usize::try_from(value).ok()),
3591            repetition_penalty: request
3592                .repetition_penalty
3593                .unwrap_or(DEFAULT_CHAT_REPETITION_PENALTY),
3594            presence_penalty: request.presence_penalty.unwrap_or(0.0),
3595            frequency_penalty: request.frequency_penalty.unwrap_or(0.0),
3596            stop_sequences: request.stop.clone().unwrap_or_default(),
3597            seed: request.seed,
3598            min_p: request.min_p.filter(|value| *value > 0.0),
3599            tfs: None,
3600            typical_p: None,
3601            mirostat: None,
3602            response_format,
3603            structured_output_start,
3604            response_completion_boundary,
3605            model_output_protocol,
3606        },
3607        stream: request.stream.unwrap_or(false),
3608        priority: Priority::Normal, // Default priority
3609        client_id: None,
3610        session_id: None,
3611        created_at: chrono::Utc::now(),
3612        api_request: Some(ferrum_types::ApiRequest::Chat(api_chat)),
3613        evidence_request: Default::default(),
3614        metadata,
3615    })
3616}
3617
3618fn push_unique_forbidden_token_text(tokens: &mut Vec<String>, token: &str) {
3619    if !token.is_empty() && !tokens.iter().any(|existing| existing == token) {
3620        tokens.push(token.to_string());
3621    }
3622}
3623
3624fn default_auto_tool_choice_for_tools(
3625    tools: &[ChatTool],
3626    choice: Option<&ToolChoice>,
3627) -> Option<ToolChoice> {
3628    if choice.is_none() && !tools.is_empty() {
3629        Some(ToolChoice::Mode("auto".to_string()))
3630    } else {
3631        None
3632    }
3633}
3634
3635fn tool_choice_none(choice: Option<&ToolChoice>) -> bool {
3636    matches!(choice, Some(ToolChoice::Mode(mode)) if mode.eq_ignore_ascii_case("none"))
3637}
3638
3639fn tool_choice_none_hides_tools(
3640    choice: Option<&ToolChoice>,
3641    model_template: Option<&ModelChatTemplate>,
3642) -> bool {
3643    tool_choice_none(choice)
3644        && model_template
3645            .map(|template| template.template.contains("tools_in_user_message"))
3646            .unwrap_or(false)
3647}
3648
3649fn chat_template_options_for_request(
3650    request: &ChatCompletionsRequest,
3651    model_template: Option<&ModelChatTemplate>,
3652    default_enable_thinking: Option<bool>,
3653) -> ferrum_types::Result<ChatTemplateOptions> {
3654    let mut options = ChatTemplateOptions::default_for_template(model_template);
3655    options.enable_thinking = default_enable_thinking;
3656    let Some(kwargs) = request.chat_template_kwargs.as_ref() else {
3657        return Ok(options);
3658    };
3659    if let Some(value) = kwargs.get("enable_thinking") {
3660        let Some(enable_thinking) = value.as_bool() else {
3661            return Err(Error::invalid_request(
3662                "chat_template_kwargs.enable_thinking must be a boolean",
3663            ));
3664        };
3665        options.enable_thinking = Some(enable_thinking);
3666    }
3667    if let Some(value) = kwargs.get("reasoning_effort") {
3668        let Some(reasoning_effort) = value.as_str() else {
3669            return Err(Error::invalid_request(
3670                "chat_template_kwargs.reasoning_effort must be one of: minimal, low, medium, high, xhigh",
3671            ));
3672        };
3673        options.reasoning_effort = Some(reasoning_effort.parse::<ReasoningEffort>().map_err(
3674            |error| {
3675                Error::invalid_request(format!("chat_template_kwargs.reasoning_effort: {error}"))
3676            },
3677        )?);
3678    }
3679    Ok(options)
3680}
3681
3682fn render_messages_with_response_format_instruction(
3683    request: &ChatCompletionsRequest,
3684    output_contract: EffectiveChatOutputContract,
3685    reasoning_enabled: bool,
3686    message_phases: Option<&[Option<AssistantMessagePhase>]>,
3687) -> (Vec<ChatMessage>, Vec<Option<AssistantMessagePhase>>) {
3688    let mut phases = message_phases
3689        .map(ToOwned::to_owned)
3690        .unwrap_or_else(|| vec![None; request.messages.len()]);
3691    debug_assert_eq!(phases.len(), request.messages.len());
3692    let Some(instruction) =
3693        response_format_prompt_instruction(request, output_contract, reasoning_enabled)
3694    else {
3695        return (request.messages.clone(), phases);
3696    };
3697    let mut messages = request.messages.clone();
3698    let leading_systems = messages
3699        .iter()
3700        .take_while(|message| message.role == MessageRole::System)
3701        .count();
3702    let mut system_parts = Vec::with_capacity(leading_systems + 1);
3703    system_parts.push(instruction);
3704    system_parts.extend(
3705        messages
3706            .drain(..leading_systems)
3707            .map(|message| message.content)
3708            .filter(|content| !content.is_empty()),
3709    );
3710    phases.drain(..leading_systems);
3711    messages.insert(
3712        0,
3713        ChatMessage {
3714            role: MessageRole::System,
3715            content: system_parts.join("\n\n"),
3716            reasoning: None,
3717            name: None,
3718            tool_calls: None,
3719            tool_call_id: None,
3720            function_call: None,
3721        },
3722    );
3723    phases.insert(0, None);
3724    (messages, phases)
3725}
3726
3727fn response_format_prompt_instruction(
3728    request: &ChatCompletionsRequest,
3729    output_contract: EffectiveChatOutputContract,
3730    reasoning_enabled: bool,
3731) -> Option<String> {
3732    if !output_contract.accepts_requested_response_format() {
3733        return None;
3734    }
3735    if let Some(format) = request.response_format.as_ref() {
3736        return match format.format_type.as_str() {
3737            "json_object" => Some(
3738                "The response_format requires a single valid JSON object. Output only JSON, with no markdown fences, no explanation, no chain-of-thought, and no extra text."
3739                    .to_string(),
3740            ),
3741            "json_schema" => {
3742                let schema = format.json_schema.as_ref()?.schema.as_ref()?;
3743                let schema_text = serde_json::to_string(schema).ok()?;
3744                Some(if reasoning_enabled {
3745                    format!(
3746                        "The response_format requires a single valid JSON value satisfying this JSON Schema. Complete any enabled reasoning before the final answer. In the final answer, output only JSON, with no markdown fences, no explanation, and no extra text. Schema: {schema_text}"
3747                    )
3748                } else {
3749                    format!(
3750                        "The response_format requires a single valid JSON value satisfying this JSON Schema. Output only JSON, with no markdown fences, no explanation, no chain-of-thought, and no extra text. Schema: {schema_text}"
3751                    )
3752                })
3753            }
3754            _ => None,
3755        };
3756    }
3757    None
3758}
3759
3760fn forced_tool_choice_response_format(
3761    request: &ChatCompletionsRequest,
3762) -> Option<ferrum_types::ResponseFormat> {
3763    let selected_tool = selected_tool_for_forced_tool_choice(request)?;
3764    let schema = guided_tool_arguments_schema(selected_tool.function.parameters.as_ref())?;
3765    serde_json::to_string(&schema)
3766        .ok()
3767        .map(ferrum_types::ResponseFormat::JsonSchema)
3768}
3769
3770fn requested_response_format_for_sampling(
3771    request: &ChatCompletionsRequest,
3772) -> ferrum_types::Result<Option<ferrum_types::ResponseFormat>> {
3773    let Some(format) = request.response_format.as_ref() else {
3774        return Ok(None);
3775    };
3776    match format.format_type.as_str() {
3777        "json_object" => Ok(Some(ferrum_types::ResponseFormat::JsonObject)),
3778        "json_schema" => {
3779            let Some(schema) = format.json_schema.as_ref() else {
3780                return Err(Error::invalid_request(
3781                    "response_format.json_schema.schema is required",
3782                ));
3783            };
3784            if !schema.strict.unwrap_or(false) {
3785                return Ok(None);
3786            }
3787            let Some(schema_value) = schema.schema.as_ref() else {
3788                return Err(Error::invalid_request(
3789                    "response_format.json_schema.schema is required",
3790                ));
3791            };
3792            serde_json::to_string(schema_value)
3793                .map(|schema| Some(ferrum_types::ResponseFormat::JsonSchema(schema)))
3794                .map_err(|err| Error::invalid_request(err.to_string()))
3795        }
3796        _ => Ok(None),
3797    }
3798}
3799
3800fn selected_tool_for_forced_tool_choice(request: &ChatCompletionsRequest) -> Option<&ChatTool> {
3801    match request.tool_choice.as_ref()? {
3802        ToolChoice::Function {
3803            tool_type,
3804            function,
3805        } if tool_type == "function" => request
3806            .tools
3807            .as_ref()?
3808            .iter()
3809            .find(|tool| tool.function.name == function.name),
3810        ToolChoice::Mode(mode) if mode.eq_ignore_ascii_case("required") => {
3811            single_function_tool(request.tools.as_deref()?)
3812        }
3813        _ => None,
3814    }
3815}
3816
3817fn guided_tool_arguments_schema(
3818    parameters: Option<&serde_json::Value>,
3819) -> Option<serde_json::Value> {
3820    let mut schema = parameters?.clone();
3821    bound_unconstrained_tool_argument_strings(
3822        &mut schema,
3823        DEFAULT_GUIDED_TOOL_ARGUMENT_STRING_MAX_LENGTH,
3824    );
3825    Some(schema)
3826}
3827
3828fn bound_unconstrained_tool_argument_strings(value: &mut serde_json::Value, default_max: u64) {
3829    match value {
3830        serde_json::Value::Object(map) => {
3831            let is_string = map
3832                .get("type")
3833                .and_then(serde_json::Value::as_str)
3834                .is_some_and(|ty| ty == "string");
3835            let has_finite_string_shape = map.contains_key("enum") || map.contains_key("maxLength");
3836            if is_string && !has_finite_string_shape {
3837                map.insert(
3838                    "maxLength".to_string(),
3839                    serde_json::Value::Number(default_max.into()),
3840                );
3841            }
3842            if let Some(properties) = map
3843                .get_mut("properties")
3844                .and_then(serde_json::Value::as_object_mut)
3845            {
3846                for property in properties.values_mut() {
3847                    bound_unconstrained_tool_argument_strings(property, default_max);
3848                }
3849            }
3850            if let Some(items) = map.get_mut("items") {
3851                bound_unconstrained_tool_argument_strings(items, default_max);
3852            }
3853        }
3854        serde_json::Value::Array(items) => {
3855            for item in items {
3856                bound_unconstrained_tool_argument_strings(item, default_max);
3857            }
3858        }
3859        _ => {}
3860    }
3861}
3862
3863fn single_function_tool(tools: &[ChatTool]) -> Option<&ChatTool> {
3864    let mut function_tools = tools.iter().filter(|tool| tool.tool_type == "function");
3865    let tool = function_tools.next()?;
3866    function_tools.next().is_none().then_some(tool)
3867}
3868
3869fn stream_text_delta(text: &str, sent_len: &mut usize) -> String {
3870    if *sent_len <= text.len() && text.is_char_boundary(*sent_len) {
3871        let delta = text[*sent_len..].to_string();
3872        *sent_len = text.len();
3873        return delta;
3874    }
3875    *sent_len = text.len();
3876    String::new()
3877}
3878
3879fn chat_api_response_from_parsed_generated_text(
3880    chat_request: &ferrum_types::ApiChatRequest,
3881    parsed: &ParsedReasoningResponse,
3882    finish_reason: FinishReason,
3883) -> Option<ferrum_types::ApiChatResponse> {
3884    parsed
3885        .reasoning
3886        .as_deref()
3887        .and_then(|reasoning| {
3888            ferrum_types::chat_api_response_from_generated_text(
3889                chat_request,
3890                reasoning,
3891                finish_reason,
3892            )
3893        })
3894        .or_else(|| {
3895            ferrum_types::chat_api_response_from_generated_text(
3896                chat_request,
3897                &parsed.content,
3898                finish_reason,
3899            )
3900        })
3901}
3902
3903fn finish_reason_allows_structured_api_response(finish_reason: FinishReason) -> bool {
3904    matches!(finish_reason, FinishReason::Stop | FinishReason::EOS)
3905}
3906
3907fn log_required_tool_choice_failure(
3908    request: &ChatCompletionsRequest,
3909    content: &str,
3910    reasoning: Option<&str>,
3911) {
3912    warn!(
3913        model = %request.model,
3914        content_len = content.len(),
3915        content_head = %log_excerpt(content, 512),
3916        reasoning_len = reasoning.map(str::len).unwrap_or(0),
3917        reasoning_head = %reasoning.map(|value| log_excerpt(value, 512)).unwrap_or_default(),
3918        "model output did not satisfy required tool_choice"
3919    );
3920}
3921
3922fn log_excerpt(value: &str, max_chars: usize) -> String {
3923    let mut out = value.chars().take(max_chars).collect::<String>();
3924    if value.chars().count() > max_chars {
3925        out.push_str("...");
3926    }
3927    out
3928}
3929
3930fn normalize_structured_response_content(
3931    request: &ChatCompletionsRequest,
3932    content: &str,
3933) -> String {
3934    match EffectiveChatOutputContract::resolve(request) {
3935        EffectiveChatOutputContract::BestEffortJsonSchemaContent => {
3936            extract_json_object_text(content)
3937                .unwrap_or_else(|| strip_markdown_json_fence(content).to_string())
3938        }
3939        EffectiveChatOutputContract::RequiredToolCall
3940        | EffectiveChatOutputContract::StrictJsonSchemaContent
3941        | EffectiveChatOutputContract::JsonObjectContent
3942        | EffectiveChatOutputContract::Text => content.to_string(),
3943    }
3944}
3945
3946fn extract_json_object_text(text: &str) -> Option<String> {
3947    let text = strip_markdown_json_fence(text.trim());
3948    if serde_json::from_str::<serde_json::Value>(&text)
3949        .ok()
3950        .filter(|value| value.is_object())
3951        .is_some()
3952    {
3953        return Some(text.to_string());
3954    }
3955
3956    let start = text.find('{')?;
3957    let mut depth = 0usize;
3958    let mut in_string = false;
3959    let mut escaped = false;
3960    for (offset, ch) in text[start..].char_indices() {
3961        if in_string {
3962            if escaped {
3963                escaped = false;
3964            } else if ch == '\\' {
3965                escaped = true;
3966            } else if ch == '"' {
3967                in_string = false;
3968            }
3969            continue;
3970        }
3971        match ch {
3972            '"' => in_string = true,
3973            '{' => depth += 1,
3974            '}' => {
3975                depth = depth.saturating_sub(1);
3976                if depth == 0 {
3977                    let end = start + offset + ch.len_utf8();
3978                    let candidate = &text[start..end];
3979                    if serde_json::from_str::<serde_json::Value>(candidate)
3980                        .ok()
3981                        .filter(|value| value.is_object())
3982                        .is_some()
3983                    {
3984                        return Some(candidate.to_string());
3985                    }
3986                }
3987            }
3988            _ => {}
3989        }
3990    }
3991    None
3992}
3993
3994fn api_chat_request(
3995    request: &ChatCompletionsRequest,
3996    effective_tool_choice: Option<&ToolChoice>,
3997    tool_call_protocol: ferrum_types::ApiToolCallProtocol,
3998) -> ferrum_types::ApiChatRequest {
3999    ferrum_types::ApiChatRequest {
4000        messages: request.messages.iter().map(api_chat_message).collect(),
4001        tools: request
4002            .tools
4003            .as_deref()
4004            .unwrap_or_default()
4005            .iter()
4006            .map(api_tool)
4007            .collect(),
4008        tool_choice: effective_tool_choice.map(api_tool_choice),
4009        tool_call_protocol,
4010        legacy_functions: request
4011            .functions
4012            .as_deref()
4013            .unwrap_or_default()
4014            .iter()
4015            .map(api_function)
4016            .collect(),
4017        legacy_function_call: request.function_call.as_ref().map(api_function_call_choice),
4018        response_format: request.response_format.as_ref().map(api_response_format),
4019        stream_options: request.stream_options.as_ref().map(|opts| {
4020            ferrum_types::ApiStreamOptions {
4021                include_usage: opts.include_usage,
4022            }
4023        }),
4024    }
4025}
4026
4027fn api_chat_message(message: &ChatMessage) -> ferrum_types::ApiChatMessage {
4028    ferrum_types::ApiChatMessage {
4029        role: match message.role {
4030            MessageRole::System => ferrum_types::ApiMessageRole::System,
4031            MessageRole::User => ferrum_types::ApiMessageRole::User,
4032            MessageRole::Assistant => ferrum_types::ApiMessageRole::Assistant,
4033            MessageRole::Function => ferrum_types::ApiMessageRole::Function,
4034            MessageRole::Tool => ferrum_types::ApiMessageRole::Tool,
4035        },
4036        content: message.content.clone(),
4037        name: message.name.clone(),
4038        tool_calls: message
4039            .tool_calls
4040            .as_deref()
4041            .unwrap_or_default()
4042            .iter()
4043            .map(api_tool_call)
4044            .collect(),
4045        tool_call_id: message.tool_call_id.clone(),
4046        function_call: message.function_call.as_ref().map(api_function_call),
4047    }
4048}
4049
4050fn api_tool(tool: &ChatTool) -> ferrum_types::ApiTool {
4051    ferrum_types::ApiTool {
4052        tool_type: tool.tool_type.clone(),
4053        function: api_function(&tool.function),
4054    }
4055}
4056
4057fn api_function(function: &ChatFunction) -> ferrum_types::ApiFunction {
4058    ferrum_types::ApiFunction {
4059        name: function.name.clone(),
4060        description: function.description.clone(),
4061        parameters: function.parameters.clone(),
4062        strict: function.strict,
4063    }
4064}
4065
4066fn api_tool_choice(choice: &ToolChoice) -> ferrum_types::ApiToolChoice {
4067    match choice {
4068        ToolChoice::Mode(mode) => ferrum_types::ApiToolChoice::Mode(mode.clone()),
4069        ToolChoice::Function {
4070            tool_type,
4071            function,
4072        } => ferrum_types::ApiToolChoice::Function {
4073            tool_type: tool_type.clone(),
4074            function: ferrum_types::ApiToolChoiceFunction {
4075                name: function.name.clone(),
4076            },
4077        },
4078    }
4079}
4080
4081fn api_function_call_choice(choice: &FunctionCallChoice) -> ferrum_types::ApiFunctionCallChoice {
4082    match choice {
4083        FunctionCallChoice::Mode(mode) => ferrum_types::ApiFunctionCallChoice::Mode(mode.clone()),
4084        FunctionCallChoice::Function { name } => {
4085            ferrum_types::ApiFunctionCallChoice::Function { name: name.clone() }
4086        }
4087    }
4088}
4089
4090fn api_tool_call(tool_call: &ChatToolCall) -> ferrum_types::ApiToolCall {
4091    ferrum_types::ApiToolCall {
4092        id: tool_call.id.clone(),
4093        tool_type: tool_call.tool_type.clone(),
4094        function: api_function_call(&tool_call.function),
4095    }
4096}
4097
4098fn api_function_call(function_call: &ChatFunctionCall) -> ferrum_types::ApiFunctionCall {
4099    ferrum_types::ApiFunctionCall {
4100        name: function_call.name.clone(),
4101        arguments: function_call.arguments.clone(),
4102    }
4103}
4104
4105fn openai_chat_message_from_api(message: &ferrum_types::ApiChatMessage) -> ChatMessage {
4106    ChatMessage {
4107        role: openai_message_role_from_api(message.role),
4108        content: message.content.clone(),
4109        reasoning: None,
4110        name: message.name.clone(),
4111        tool_calls: if message.tool_calls.is_empty() {
4112            None
4113        } else {
4114            Some(
4115                message
4116                    .tool_calls
4117                    .iter()
4118                    .map(openai_tool_call_from_api)
4119                    .collect(),
4120            )
4121        },
4122        tool_call_id: message.tool_call_id.clone(),
4123        function_call: message
4124            .function_call
4125            .as_ref()
4126            .map(openai_function_call_from_api),
4127    }
4128}
4129
4130fn openai_message_role_from_api(role: ferrum_types::ApiMessageRole) -> MessageRole {
4131    match role {
4132        ferrum_types::ApiMessageRole::System => MessageRole::System,
4133        ferrum_types::ApiMessageRole::User => MessageRole::User,
4134        ferrum_types::ApiMessageRole::Assistant => MessageRole::Assistant,
4135        ferrum_types::ApiMessageRole::Function => MessageRole::Function,
4136        ferrum_types::ApiMessageRole::Tool => MessageRole::Tool,
4137    }
4138}
4139
4140fn openai_tool_call_from_api(tool_call: &ferrum_types::ApiToolCall) -> ChatToolCall {
4141    ChatToolCall {
4142        index: None,
4143        id: tool_call.id.clone(),
4144        tool_type: tool_call.tool_type.clone(),
4145        function: openai_function_call_from_api(&tool_call.function),
4146    }
4147}
4148
4149fn openai_tool_call_delta_from_api(
4150    index: usize,
4151    tool_call: &ferrum_types::ApiToolCall,
4152) -> ChatToolCall {
4153    ChatToolCall {
4154        index: Some(usize_to_u32_saturating(index)),
4155        id: tool_call.id.clone(),
4156        tool_type: tool_call.tool_type.clone(),
4157        function: openai_function_call_from_api(&tool_call.function),
4158    }
4159}
4160
4161fn openai_chat_delta_from_api(message: &ferrum_types::ApiChatMessage) -> ChatMessage {
4162    let mut delta = openai_chat_message_from_api(message);
4163    if !message.tool_calls.is_empty() {
4164        delta.tool_calls = Some(
4165            message
4166                .tool_calls
4167                .iter()
4168                .enumerate()
4169                .map(|(index, call)| openai_tool_call_delta_from_api(index, call))
4170                .collect(),
4171        );
4172    }
4173    delta
4174}
4175
4176fn openai_function_call_from_api(
4177    function_call: &ferrum_types::ApiFunctionCall,
4178) -> ChatFunctionCall {
4179    ChatFunctionCall {
4180        name: function_call.name.clone(),
4181        arguments: function_call.arguments.clone(),
4182    }
4183}
4184
4185fn api_response_format(format: &OpenAiResponseFormat) -> ferrum_types::ApiResponseFormat {
4186    ferrum_types::ApiResponseFormat {
4187        format_type: format.format_type.clone(),
4188        json_schema: format
4189            .json_schema
4190            .as_ref()
4191            .map(|schema| ferrum_types::ApiJsonSchema {
4192                name: schema.name.clone(),
4193                schema: schema.schema.clone().unwrap_or(serde_json::Value::Null),
4194                strict: schema.strict,
4195            }),
4196    }
4197}
4198
4199fn validate_chat_request(request: &ChatCompletionsRequest) -> std::result::Result<(), ServerError> {
4200    if request.messages.is_empty() {
4201        return Err(ServerError::invalid_request(
4202            "messages array must not be empty",
4203            Some("messages"),
4204        ));
4205    }
4206
4207    if let Some(n) = request.n {
4208        if n != 1 {
4209            return Err(ServerError::unsupported_feature(
4210                "only n=1 is supported for chat completions",
4211                Some("n"),
4212            ));
4213        }
4214    }
4215
4216    if request
4217        .logit_bias
4218        .as_ref()
4219        .is_some_and(|bias| !bias.is_empty())
4220    {
4221        return Err(ServerError::unsupported_feature(
4222            "logit_bias is not supported",
4223            Some("logit_bias"),
4224        ));
4225    }
4226    if request.logprobs.unwrap_or(false) {
4227        return Err(ServerError::unsupported_feature(
4228            "logprobs is not supported",
4229            Some("logprobs"),
4230        ));
4231    }
4232    if request.top_logprobs.unwrap_or(0) > 0 {
4233        return Err(ServerError::unsupported_feature(
4234            "top_logprobs is not supported",
4235            Some("top_logprobs"),
4236        ));
4237    }
4238
4239    if let Some(top_k) = request.top_k {
4240        if top_k < -1 {
4241            return Err(ServerError::invalid_request(
4242                "top_k must be -1, 0, or a positive integer",
4243                Some("top_k"),
4244            ));
4245        }
4246    }
4247    if let Some(min_p) = request.min_p {
4248        if !min_p.is_finite() || !(0.0..=1.0).contains(&min_p) {
4249            return Err(ServerError::invalid_request(
4250                "min_p must be in range [0, 1]",
4251                Some("min_p"),
4252            ));
4253        }
4254    }
4255    if let Some(repetition_penalty) = request.repetition_penalty {
4256        if !repetition_penalty.is_finite() || repetition_penalty <= 0.0 {
4257            return Err(ServerError::invalid_request(
4258                "repetition_penalty must be positive",
4259                Some("repetition_penalty"),
4260            ));
4261        }
4262    }
4263    if let Some(presence_penalty) = request.presence_penalty {
4264        if !presence_penalty.is_finite() || !(-2.0..=2.0).contains(&presence_penalty) {
4265            return Err(ServerError::invalid_request(
4266                "presence_penalty must be in range [-2, 2]",
4267                Some("presence_penalty"),
4268            ));
4269        }
4270    }
4271    if let Some(frequency_penalty) = request.frequency_penalty {
4272        if !frequency_penalty.is_finite() || !(-2.0..=2.0).contains(&frequency_penalty) {
4273            return Err(ServerError::invalid_request(
4274                "frequency_penalty must be in range [-2, 2]",
4275                Some("frequency_penalty"),
4276            ));
4277        }
4278    }
4279
4280    if request.stream_options.is_some() && !request.stream.unwrap_or(false) {
4281        return Err(ServerError::invalid_request(
4282            "stream_options is only valid when stream=true",
4283            Some("stream_options"),
4284        ));
4285    }
4286    ensure_response_format_supported(request)?;
4287
4288    if let Some(tools) = &request.tools {
4289        for tool in tools {
4290            if tool.tool_type != "function" {
4291                return Err(ServerError::unsupported_feature(
4292                    "only function tools are supported",
4293                    Some("tools"),
4294                ));
4295            }
4296        }
4297    }
4298
4299    if let Some(choice) = &request.tool_choice {
4300        match choice {
4301            ToolChoice::Mode(mode)
4302                if mode.eq_ignore_ascii_case("auto") || mode.eq_ignore_ascii_case("none") => {}
4303            ToolChoice::Mode(mode) if mode.eq_ignore_ascii_case("required") => {
4304                if request.tools.as_deref().unwrap_or_default().is_empty() {
4305                    return Err(ServerError::invalid_request(
4306                        "tool_choice=required requires at least one function tool",
4307                        Some("tool_choice"),
4308                    ));
4309                }
4310            }
4311            ToolChoice::Mode(_) => {
4312                return Err(ServerError::unsupported_feature(
4313                    "unsupported tool_choice mode",
4314                    Some("tool_choice"),
4315                ));
4316            }
4317            ToolChoice::Function {
4318                tool_type,
4319                function,
4320            } => {
4321                if tool_type != "function" {
4322                    return Err(ServerError::unsupported_feature(
4323                        "only function tool_choice is supported",
4324                        Some("tool_choice"),
4325                    ));
4326                }
4327                let declared = request
4328                    .tools
4329                    .as_deref()
4330                    .unwrap_or_default()
4331                    .iter()
4332                    .any(|tool| tool.function.name == function.name);
4333                if !declared {
4334                    return Err(ServerError::invalid_request(
4335                        "tool_choice selects a function that is not declared in tools",
4336                        Some("tool_choice"),
4337                    ));
4338                }
4339            }
4340        }
4341    }
4342
4343    if let Some(choice) = &request.function_call {
4344        match choice {
4345            FunctionCallChoice::Mode(mode)
4346                if mode.eq_ignore_ascii_case("auto") || mode.eq_ignore_ascii_case("none") => {}
4347            FunctionCallChoice::Mode(_) => {
4348                return Err(ServerError::unsupported_feature(
4349                    "unsupported function_call mode",
4350                    Some("function_call"),
4351                ));
4352            }
4353            FunctionCallChoice::Function { name } => {
4354                let declared = request
4355                    .functions
4356                    .as_deref()
4357                    .unwrap_or_default()
4358                    .iter()
4359                    .any(|function| function.name == *name);
4360                if !declared {
4361                    return Err(ServerError::invalid_request(
4362                        "function_call selects a function that is not declared in functions",
4363                        Some("function_call"),
4364                    ));
4365                }
4366            }
4367        }
4368    }
4369
4370    Ok(())
4371}
4372
4373fn tool_choice_required(request: &ChatCompletionsRequest) -> bool {
4374    match request.tool_choice.as_ref() {
4375        Some(ToolChoice::Mode(mode)) if mode.eq_ignore_ascii_case("required") => true,
4376        Some(ToolChoice::Function {
4377            tool_type,
4378            function,
4379        }) => {
4380            tool_type == "function"
4381                && request
4382                    .tools
4383                    .as_deref()
4384                    .unwrap_or_default()
4385                    .iter()
4386                    .any(|tool| tool.function.name == function.name)
4387        }
4388        _ => false,
4389    }
4390}
4391
4392fn openai_usage_from_token_usage(usage: &TokenUsage) -> Usage {
4393    let prompt_tokens = usize_to_u32_saturating(usage.prompt_tokens);
4394    let completion_tokens = usize_to_u32_saturating(usage.completion_tokens);
4395    let total_tokens = usize_to_u32_saturating(usage.total_tokens);
4396    Usage {
4397        prompt_tokens,
4398        completion_tokens,
4399        total_tokens,
4400    }
4401}
4402
4403fn usize_to_u32_saturating(value: usize) -> u32 {
4404    u32::try_from(value).unwrap_or(u32::MAX)
4405}
4406
4407fn ensure_response_format_supported(
4408    request: &ChatCompletionsRequest,
4409) -> std::result::Result<(), ServerError> {
4410    if let Some(rf) = &request.response_format {
4411        match rf.format_type.as_str() {
4412            "text" | "json_object" => {}
4413            "json_schema" => {
4414                let Some(schema_config) = rf.json_schema.as_ref() else {
4415                    return Err(ServerError::invalid_request(
4416                        "response_format.json_schema.schema is required",
4417                        Some("response_format.json_schema"),
4418                    ));
4419                };
4420                let Some(schema) = schema_config.schema.as_ref() else {
4421                    return Err(ServerError::invalid_request(
4422                        "response_format.json_schema.schema is required",
4423                        Some("response_format.json_schema"),
4424                    ));
4425                };
4426                if schema_config.strict.unwrap_or(false) {
4427                    compiled_json_schema_validator(schema).map_err(|reason| {
4428                        ServerError::invalid_request(
4429                            format!("unsupported strict json_schema: {reason}"),
4430                            Some("response_format.json_schema"),
4431                        )
4432                    })?;
4433                }
4434            }
4435            _ => {
4436                return Err(ServerError::invalid_request(
4437                    "unsupported response_format.type",
4438                    Some("response_format.type"),
4439                ));
4440            }
4441        }
4442    }
4443    Ok(())
4444}
4445
4446fn strict_json_schema_string(
4447    request: &ChatCompletionsRequest,
4448) -> std::result::Result<Option<String>, ServerError> {
4449    let Some(rf) = &request.response_format else {
4450        return Ok(None);
4451    };
4452    if rf.format_type != "json_schema" {
4453        return Ok(None);
4454    }
4455    let Some(schema) = &rf.json_schema else {
4456        return Err(ServerError::invalid_request(
4457            "response_format.json_schema.schema is required",
4458            Some("response_format.json_schema"),
4459        ));
4460    };
4461    let Some(schema_value) = schema.schema.as_ref() else {
4462        return Err(ServerError::invalid_request(
4463            "response_format.json_schema.schema is required",
4464            Some("response_format.json_schema"),
4465        ));
4466    };
4467    if !schema.strict.unwrap_or(false) {
4468        return Ok(None);
4469    }
4470    serde_json::to_string(schema_value).map(Some).map_err(|e| {
4471        ServerError::invalid_request(e.to_string(), Some("response_format.json_schema"))
4472    })
4473}
4474
4475fn validate_hard_structured_response(
4476    request: &ChatCompletionsRequest,
4477    content: &str,
4478) -> std::result::Result<(), ServerError> {
4479    match EffectiveChatOutputContract::resolve(request) {
4480        EffectiveChatOutputContract::JsonObjectContent => {
4481            let value = serde_json::from_str::<serde_json::Value>(content).map_err(|error| {
4482                ServerError::InternalError(format!(
4483                    "model output did not satisfy response_format.json_object: invalid JSON: {error}"
4484                ))
4485            })?;
4486            if !value.is_object() {
4487                return Err(ServerError::InternalError(
4488                    "model output did not satisfy response_format.json_object: root must be an object"
4489                        .to_string(),
4490                ));
4491            }
4492            Ok(())
4493        }
4494        EffectiveChatOutputContract::StrictJsonSchemaContent => {
4495            let Some(schema_json) = strict_json_schema_string(request)? else {
4496                return Ok(());
4497            };
4498            let schema: serde_json::Value = serde_json::from_str(&schema_json).map_err(|e| {
4499                ServerError::InternalError(format!(
4500                    "strict json_schema could not be reconstructed after request validation: {e}"
4501                ))
4502            })?;
4503            validate_json_text_against_schema(&schema, content).map_err(|reason| {
4504                ServerError::InternalError(format!(
4505                    "model output did not satisfy response_format.json_schema.strict: {reason}"
4506                ))
4507            })
4508        }
4509        EffectiveChatOutputContract::RequiredToolCall
4510        | EffectiveChatOutputContract::BestEffortJsonSchemaContent
4511        | EffectiveChatOutputContract::Text => Ok(()),
4512    }
4513}
4514
4515fn structured_response_error_param(contract: EffectiveChatOutputContract) -> Option<&'static str> {
4516    match contract {
4517        EffectiveChatOutputContract::JsonObjectContent => Some("response_format"),
4518        EffectiveChatOutputContract::StrictJsonSchemaContent => Some("response_format.json_schema"),
4519        _ => None,
4520    }
4521}
4522
4523fn validate_structured_tool_response(
4524    request: &ChatCompletionsRequest,
4525    response: &ferrum_types::ApiChatResponse,
4526) -> std::result::Result<(), ServerError> {
4527    let required = tool_choice_required(request);
4528    if response.message.tool_calls.is_empty() {
4529        if required {
4530            return Err(ServerError::invalid_request(
4531                "model output did not satisfy required tool_choice",
4532                Some("tool_choice"),
4533            ));
4534        }
4535        return Ok(());
4536    }
4537
4538    if tool_choice_none(request.tool_choice.as_ref()) {
4539        return Err(ServerError::InternalError(
4540            "model emitted a tool call while tool_choice is 'none'".to_string(),
4541        ));
4542    }
4543
4544    if required {
4545        if !response.message.content.trim().is_empty() {
4546            return Err(ServerError::InternalError(
4547                "required tool response contained assistant content".to_string(),
4548            ));
4549        }
4550        if response.finish_reason.as_deref() != Some("tool_calls") {
4551            return Err(ServerError::InternalError(
4552                "required tool response did not finish with tool_calls".to_string(),
4553            ));
4554        }
4555    }
4556
4557    let tools = request.tools.as_deref().unwrap_or_default();
4558    for call in &response.message.tool_calls {
4559        if call.tool_type != "function" {
4560            return Err(ServerError::InternalError(format!(
4561                "model emitted unsupported tool call type '{}'",
4562                call.tool_type
4563            )));
4564        }
4565        let Some(tool) = tools
4566            .iter()
4567            .find(|tool| tool.tool_type == "function" && tool.function.name == call.function.name)
4568        else {
4569            return Err(ServerError::InternalError(format!(
4570                "model emitted undeclared tool call '{}'",
4571                call.function.name
4572            )));
4573        };
4574        if let Some(ToolChoice::Function {
4575            tool_type,
4576            function,
4577        }) = request.tool_choice.as_ref()
4578        {
4579            if tool_type != "function" || function.name != call.function.name {
4580                return Err(ServerError::InternalError(format!(
4581                    "model emitted tool '{}' instead of selected tool '{}'",
4582                    call.function.name, function.name
4583                )));
4584            }
4585        }
4586
4587        let arguments: serde_json::Value =
4588            serde_json::from_str(&call.function.arguments).map_err(|e| {
4589                ServerError::InternalError(format!(
4590                    "model emitted invalid JSON arguments for tool '{}': {e}",
4591                    call.function.name
4592                ))
4593            })?;
4594        if !arguments.is_object() {
4595            return Err(ServerError::InternalError(format!(
4596                "model emitted non-object arguments for tool '{}'",
4597                call.function.name
4598            )));
4599        }
4600        if let Some(schema) = tool.function.parameters.as_ref() {
4601            validate_json_text_against_schema(schema, &call.function.arguments).map_err(
4602                |reason| {
4603                    ServerError::InternalError(format!(
4604                        "model arguments for tool '{}' did not satisfy its schema: {reason}",
4605                        call.function.name
4606                    ))
4607                },
4608            )?;
4609        }
4610    }
4611    Ok(())
4612}
4613
4614fn validate_json_text_against_schema(
4615    schema: &serde_json::Value,
4616    content: &str,
4617) -> std::result::Result<(), String> {
4618    let value = serde_json::from_str::<serde_json::Value>(content)
4619        .map_err(|e| format!("invalid JSON: {e}"))?;
4620    compiled_json_schema_validator(schema)?
4621        .validate(&value)
4622        .map_err(|error| error.to_string())
4623}
4624
4625fn compiled_json_schema_validator(
4626    schema: &serde_json::Value,
4627) -> std::result::Result<Arc<jsonschema::Validator>, String> {
4628    let cache_key = serde_json::to_string(schema)
4629        .map_err(|error| format!("could not serialize JSON Schema: {error}"))?;
4630    let cache = JSON_SCHEMA_VALIDATOR_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
4631    let mut validators = cache
4632        .lock()
4633        .map_err(|_| "JSON Schema validator cache lock was poisoned".to_string())?;
4634    if let Some(validator) = validators.get(&cache_key) {
4635        return Ok(Arc::clone(validator));
4636    }
4637
4638    let validator = Arc::new(
4639        jsonschema::validator_for(schema)
4640            .map_err(|error| format!("could not compile JSON Schema: {error}"))?,
4641    );
4642    if validators.len() >= MAX_CACHED_JSON_SCHEMA_VALIDATORS {
4643        validators.clear();
4644    }
4645    validators.insert(cache_key, Arc::clone(&validator));
4646    Ok(validator)
4647}
4648
4649fn stream_validation_error_message(error: ServerError) -> String {
4650    match error {
4651        ServerError::InternalError(message)
4652        | ServerError::NotImplemented(message)
4653        | ServerError::ServiceUnavailable(message)
4654        | ServerError::InvalidRequest { message, .. }
4655        | ServerError::UnsupportedFeature { message, .. } => message,
4656    }
4657}
4658
4659fn server_error_from_ferrum_error(error: Error) -> ServerError {
4660    match error {
4661        Error::RequestValidation { message } => ServerError::invalid_request(message, None),
4662        Error::ResourceExhausted { message } => ServerError::ServiceUnavailable(message),
4663        other => ServerError::InternalError(other.to_string()),
4664    }
4665}
4666
4667fn stream_error_payload(
4668    message: impl Into<String>,
4669    error_type: &str,
4670    param: Option<&str>,
4671) -> OpenAiError {
4672    OpenAiError {
4673        error: OpenAiErrorDetail {
4674            message: message.into(),
4675            error_type: error_type.to_string(),
4676            param: param.map(str::to_string),
4677            code: None,
4678        },
4679    }
4680}
4681
4682fn openai_error_sse_event(
4683    message: impl Into<String>,
4684    error_type: &str,
4685    param: Option<&str>,
4686) -> Event {
4687    Event::default()
4688        .json_data(&stream_error_payload(message, error_type, param))
4689        .unwrap_or_else(|_| Event::default().data("error"))
4690}
4691
4692fn convert_completion_request(request: &CompletionsRequest) -> InferenceRequest {
4693    let prompt = request
4694        .prompt
4695        .as_text()
4696        .expect("completion prompt validated before conversion");
4697    InferenceRequest {
4698        id: RequestId(Uuid::new_v4()),
4699        model_id: ModelId(request.model.clone()),
4700        prompt: prompt.to_string(),
4701        sampling_params: SamplingParams {
4702            max_tokens: request.max_tokens.unwrap_or(DEFAULT_COMPLETION_MAX_TOKENS) as usize,
4703            temperature: request.temperature.unwrap_or(DEFAULT_SAMPLING_TEMPERATURE),
4704            top_p: request.top_p.unwrap_or(DEFAULT_SAMPLING_TOP_P),
4705            top_k: None,
4706            repetition_penalty: 1.0,
4707            presence_penalty: 0.0,
4708            frequency_penalty: 0.0,
4709            stop_sequences: request.stop.clone().unwrap_or_default(),
4710            seed: None,
4711            min_p: None,
4712            tfs: None,
4713            typical_p: None,
4714            mirostat: None,
4715            response_format: ferrum_types::ResponseFormat::Text,
4716            structured_output_start: StructuredOutputStart::Immediate,
4717            response_completion_boundary: ResponseCompletionBoundary::Immediate,
4718            model_output_protocol: ferrum_types::ModelOutputProtocol::Text,
4719        },
4720        stream: request.stream.unwrap_or(false),
4721        priority: Priority::Normal,
4722        client_id: None,
4723        session_id: None,
4724        created_at: chrono::Utc::now(),
4725        api_request: Some(ferrum_types::ApiRequest::Completion(
4726            ferrum_types::ApiCompletionRequest {
4727                prompt: prompt.to_string(),
4728                response_format: None,
4729            },
4730        )),
4731        evidence_request: Default::default(),
4732        metadata: if request.max_tokens.is_none() {
4733            HashMap::from([(
4734                DEFAULT_MAX_TOKENS_METADATA_KEY.to_string(),
4735                serde_json::json!(true),
4736            )])
4737        } else {
4738            HashMap::new()
4739        },
4740    }
4741}
4742
4743fn resolve_request_model<'a>(
4744    registry: &'a ServedModelRegistry,
4745    request_model: &str,
4746    required_kind: ServedModelKind,
4747) -> std::result::Result<(ModelId, Option<&'a LoraAdapterModel>), ServerError> {
4748    if registry.is_empty() {
4749        return Ok((ModelId::new(request_model), None));
4750    }
4751    let entry = registry
4752        .resolve(request_model, required_kind)
4753        .ok_or_else(|| {
4754            ServerError::invalid_request(format!("unknown model: {request_model}"), Some("model"))
4755        })?;
4756    Ok((entry.engine_model_id().clone(), entry.adapter()))
4757}
4758
4759fn apply_served_model_resolution(
4760    inference_request: &mut InferenceRequest,
4761    engine_model_id: ModelId,
4762    adapter: Option<&LoraAdapterModel>,
4763) {
4764    inference_request.model_id = engine_model_id;
4765    if let Some(adapter) = adapter {
4766        inference_request.metadata.insert(
4767            "ferrum_lora_adapter".to_string(),
4768            serde_json::json!(adapter.name),
4769        );
4770        inference_request.metadata.insert(
4771            "ferrum_lora_model_id".to_string(),
4772            serde_json::json!(adapter.model_id),
4773        );
4774        inference_request.metadata.insert(
4775            "ferrum_lora_path".to_string(),
4776            serde_json::json!(adapter.path),
4777        );
4778    }
4779}
4780
4781async fn handle_completions_sync(
4782    state: AppState,
4783    openai_request: CompletionsRequest,
4784    inference_request: InferenceRequest,
4785) -> std::result::Result<Response, ServerError> {
4786    let engine = state.llm.clone().ok_or_else(|| {
4787        ServerError::ServiceUnavailable("LLM engine not loaded; completions unavailable".into())
4788    })?;
4789    match engine.infer(inference_request).await {
4790        Ok(output) => {
4791            let InferenceResponse {
4792                text: output_text,
4793                finish_reason,
4794                usage,
4795                api_response,
4796                ..
4797            } = output;
4798            let stop_sequences = openai_request.stop.clone().unwrap_or_default();
4799            let mut text = strip_after_stop(&output_text, &stop_sequences);
4800            let mut openai_finish_reason = finish_reason_to_string(&finish_reason);
4801            if let Some(ferrum_types::ApiResponse::Completion(completion_response)) =
4802                api_response.as_ref()
4803            {
4804                text = strip_after_stop(&completion_response.text, &stop_sequences);
4805                if let Some(reason) = &completion_response.finish_reason {
4806                    openai_finish_reason = reason.clone();
4807                }
4808            }
4809            let response = CompletionsResponse {
4810                id: Uuid::new_v4().to_string(),
4811                object: "text_completion".to_string(),
4812                created: chrono::Utc::now().timestamp() as u64,
4813                model: openai_request.model,
4814                choices: vec![CompletionChoice {
4815                    text,
4816                    index: 0,
4817                    finish_reason: Some(openai_finish_reason),
4818                }],
4819                usage: Some(openai_usage_from_token_usage(&usage)),
4820            };
4821            Ok(Json(response).into_response())
4822        }
4823        Err(e) => {
4824            error!("Completion generation failed: {}", e);
4825            Err(ServerError::InternalError(e.to_string()))
4826        }
4827    }
4828}
4829
4830async fn handle_completions_stream(
4831    state: AppState,
4832    openai_request: CompletionsRequest,
4833    inference_request: InferenceRequest,
4834) -> std::result::Result<Response, ServerError> {
4835    let (tx, rx) = mpsc::unbounded_channel::<std::result::Result<Event, axum::Error>>();
4836    let engine = state.llm.clone().ok_or_else(|| {
4837        ServerError::ServiceUnavailable("LLM engine not loaded; completions unavailable".into())
4838    })?;
4839    let request_id = Uuid::new_v4().to_string();
4840
4841    tokio::spawn(async move {
4842        match engine.infer_stream(inference_request).await {
4843            Ok(mut stream) => {
4844                while let Some(result) = stream.next().await {
4845                    match result {
4846                        Ok(chunk) => {
4847                            let response_chunk = CompletionsResponse {
4848                                id: request_id.clone(),
4849                                object: "text_completion".to_string(),
4850                                created: chrono::Utc::now().timestamp() as u64,
4851                                model: openai_request.model.clone(),
4852                                choices: vec![CompletionChoice {
4853                                    text: chunk.text.clone(),
4854                                    index: 0,
4855                                    finish_reason: chunk
4856                                        .finish_reason
4857                                        .as_ref()
4858                                        .map(finish_reason_to_string),
4859                                }],
4860                                usage: None,
4861                            };
4862                            let event = Event::default()
4863                                .json_data(&response_chunk)
4864                                .unwrap_or_else(|_| Event::default().data("error"));
4865                            if tx.send(Ok(event)).is_err() {
4866                                break;
4867                            }
4868                            if chunk.finish_reason.is_some() {
4869                                if let Some(usage) =
4870                                    chunk.usage.as_ref().map(openai_usage_from_token_usage)
4871                                {
4872                                    let final_chunk = CompletionsResponse {
4873                                        id: request_id.clone(),
4874                                        object: "text_completion".to_string(),
4875                                        created: chrono::Utc::now().timestamp() as u64,
4876                                        model: openai_request.model.clone(),
4877                                        choices: vec![],
4878                                        usage: Some(usage),
4879                                    };
4880                                    let event = Event::default()
4881                                        .json_data(&final_chunk)
4882                                        .unwrap_or_else(|_| Event::default().data("error"));
4883                                    let _ = tx.send(Ok(event));
4884                                }
4885                                let _ = tx.send(Ok(Event::default().data("[DONE]")));
4886                                break;
4887                            }
4888                        }
4889                        Err(e) => {
4890                            error!("Completion stream generation error: {}", e);
4891                            let _ = tx.send(Ok(openai_error_sse_event(
4892                                e.to_string(),
4893                                "internal_server_error",
4894                                None,
4895                            )));
4896                            let _ = tx.send(Ok(Event::default().data("[DONE]")));
4897                            break;
4898                        }
4899                    }
4900                }
4901            }
4902            Err(e) => {
4903                error!("Failed to start completion stream: {}", e);
4904                let _ = tx.send(Ok(openai_error_sse_event(
4905                    e.to_string(),
4906                    "internal_server_error",
4907                    None,
4908                )));
4909                let _ = tx.send(Ok(Event::default().data("[DONE]")));
4910            }
4911        }
4912    });
4913
4914    let stream = tokio_stream::wrappers::UnboundedReceiverStream::new(rx);
4915    Ok(Sse::new(stream).into_response())
4916}
4917
4918/// Other handlers
4919async fn completions_handler(
4920    State(state): State<AppState>,
4921    request: std::result::Result<Json<CompletionsRequest>, JsonRejection>,
4922) -> std::result::Result<Response, ServerError> {
4923    let Json(request) = request.map_err(|e| {
4924        ServerError::invalid_request(format!("invalid completions request: {e}"), None)
4925    })?;
4926    validate_completion_request(&request)?;
4927    let (engine_model_id, lora_adapter) = resolve_request_model(
4928        &state.served_model_registry,
4929        &request.model,
4930        ServedModelKind::Llm,
4931    )?;
4932    let mut inference_request = convert_completion_request(&request);
4933    apply_served_model_resolution(&mut inference_request, engine_model_id, lora_adapter);
4934    if request.stream.unwrap_or(false) {
4935        handle_completions_stream(state, request, inference_request).await
4936    } else {
4937        handle_completions_sync(state, request, inference_request).await
4938    }
4939}
4940
4941fn validate_completion_request(
4942    request: &CompletionsRequest,
4943) -> std::result::Result<(), ServerError> {
4944    if request.prompt.as_text().is_none() {
4945        return Err(ServerError::invalid_request(
4946            "only string prompt is supported for completions",
4947            Some("prompt"),
4948        ));
4949    }
4950    if let Some(n) = request.n {
4951        if n != 1 {
4952            return Err(ServerError::unsupported_feature(
4953                "only n=1 is supported for completions",
4954                Some("n"),
4955            ));
4956        }
4957    }
4958    if request.logprobs.is_some() {
4959        return Err(ServerError::unsupported_feature(
4960            "logprobs is not supported for completions",
4961            Some("logprobs"),
4962        ));
4963    }
4964    if request
4965        .logit_bias
4966        .as_ref()
4967        .is_some_and(|bias| !bias.is_empty())
4968    {
4969        return Err(ServerError::unsupported_feature(
4970            "logit_bias is not supported",
4971            Some("logit_bias"),
4972        ));
4973    }
4974    Ok(())
4975}
4976
4977/// Embeddings handler — text and image embedding via OpenAI-compatible API.
4978async fn embeddings_handler(
4979    State(state): State<AppState>,
4980    request: std::result::Result<Json<EmbeddingsRequest>, JsonRejection>,
4981) -> std::result::Result<Response, ServerError> {
4982    let Json(request) = request.map_err(|e| {
4983        ServerError::invalid_request(format!("invalid embeddings request: {e}"), None)
4984    })?;
4985
4986    let span = span!(Level::INFO, "embeddings", model = %request.model);
4987    let _enter = span.enter();
4988
4989    validate_embeddings_request(&request)?;
4990    resolve_request_model(
4991        &state.served_model_registry,
4992        &request.model,
4993        ServedModelKind::Embedding,
4994    )?;
4995
4996    // Flatten input into individual items
4997    let items: Vec<EmbeddingItem> = match request.input {
4998        EmbeddingInput::Single(text) => vec![EmbeddingItem {
4999            text: Some(text),
5000            image: None,
5001        }],
5002        EmbeddingInput::Batch(texts) => texts
5003            .into_iter()
5004            .map(|t| EmbeddingItem {
5005                text: Some(t),
5006                image: None,
5007            })
5008            .collect(),
5009        EmbeddingInput::SingleObject(item) => vec![item],
5010        EmbeddingInput::BatchObjects(items) => items,
5011    };
5012
5013    if items.is_empty() {
5014        return Err(ServerError::invalid_request(
5015            "input must not be empty",
5016            Some("input"),
5017        ));
5018    }
5019
5020    let mut data = Vec::with_capacity(items.len());
5021    let mut total_tokens = 0u32;
5022
5023    let engine = state.embed.as_ref().ok_or_else(|| {
5024        ServerError::NotImplemented("Embed engine not loaded; embeddings unavailable".into())
5025    })?;
5026    for (idx, item) in items.iter().enumerate() {
5027        let embedding = if let Some(ref image) = item.image {
5028            engine
5029                .embed_image(image)
5030                .await
5031                .map_err(|e| ServerError::InternalError(format!("embed_image: {e}")))?
5032        } else if let Some(ref text) = item.text {
5033            total_tokens += text.len() as u32;
5034            engine
5035                .embed_text(text)
5036                .await
5037                .map_err(|e| ServerError::InternalError(format!("embed_text: {e}")))?
5038        } else {
5039            return Err(ServerError::invalid_request(
5040                "each input item must have either text or image",
5041                Some("input"),
5042            ));
5043        };
5044
5045        data.push(EmbeddingData {
5046            object: "embedding".to_string(),
5047            embedding,
5048            index: idx,
5049        });
5050    }
5051
5052    let response = EmbeddingsResponse {
5053        object: "list".to_string(),
5054        data,
5055        model: request.model,
5056        usage: EmbeddingUsage {
5057            prompt_tokens: total_tokens,
5058            total_tokens,
5059        },
5060    };
5061
5062    Ok(Json(response).into_response())
5063}
5064
5065fn validate_embeddings_request(
5066    request: &EmbeddingsRequest,
5067) -> std::result::Result<(), ServerError> {
5068    if let Some(format) = request.encoding_format.as_deref() {
5069        if !format.eq_ignore_ascii_case("float") {
5070            return Err(ServerError::unsupported_feature(
5071                "only encoding_format=float is supported for embeddings",
5072                Some("encoding_format"),
5073            ));
5074        }
5075    }
5076    Ok(())
5077}
5078
5079/// Audio transcription handler (OpenAI-compatible multipart form).
5080async fn transcriptions_handler(
5081    State(state): State<AppState>,
5082    multipart: std::result::Result<axum::extract::Multipart, MultipartRejection>,
5083) -> std::result::Result<Response, ServerError> {
5084    let mut multipart = multipart.map_err(|e| {
5085        ServerError::invalid_request(format!("invalid transcriptions request: {e}"), None)
5086    })?;
5087
5088    let span = span!(Level::INFO, "transcription");
5089    let _enter = span.enter();
5090
5091    let mut file_data: Option<Vec<u8>> = None;
5092    let mut language: Option<String> = None;
5093    let mut response_format: Option<String> = None;
5094
5095    while let Some(field) = multipart
5096        .next_field()
5097        .await
5098        .map_err(|e| ServerError::invalid_request(format!("multipart: {e}"), None))?
5099    {
5100        let name = field.name().unwrap_or("").to_string();
5101        match name.as_str() {
5102            "file" => {
5103                file_data = Some(
5104                    field
5105                        .bytes()
5106                        .await
5107                        .map_err(|e| {
5108                            ServerError::invalid_request(format!("read file: {e}"), Some("file"))
5109                        })?
5110                        .to_vec(),
5111                );
5112            }
5113            "language" => {
5114                language = field.text().await.ok().filter(|s| !s.is_empty());
5115            }
5116            "response_format" => {
5117                response_format = field.text().await.ok().filter(|s| !s.is_empty());
5118            }
5119            _ => {} // ignore model and other optional multipart fields for now
5120        }
5121    }
5122
5123    validate_transcription_response_format(response_format.as_deref())?;
5124
5125    let data = file_data
5126        .ok_or_else(|| ServerError::invalid_request("missing file field", Some("file")))?;
5127
5128    let engine = state.transcribe.as_ref().ok_or_else(|| {
5129        ServerError::NotImplemented("Transcribe engine not loaded; ASR unavailable".into())
5130    })?;
5131    let text = engine
5132        .transcribe_bytes(&data, language.as_deref())
5133        .await
5134        .map_err(|e| ServerError::InternalError(format!("transcribe: {e}")))?;
5135
5136    Ok(Json(TranscriptionResponse { text }).into_response())
5137}
5138
5139fn validate_transcription_response_format(
5140    response_format: Option<&str>,
5141) -> std::result::Result<(), ServerError> {
5142    if let Some(format) = response_format {
5143        if !format.eq_ignore_ascii_case("json") {
5144            return Err(ServerError::unsupported_feature(
5145                "only response_format=json is supported for transcriptions",
5146                Some("response_format"),
5147            ));
5148        }
5149    }
5150    Ok(())
5151}
5152
5153/// TTS speech synthesis handler (OpenAI-compatible /v1/audio/speech)
5154async fn speech_handler(
5155    State(state): State<AppState>,
5156    request: std::result::Result<Json<SpeechRequest>, JsonRejection>,
5157) -> std::result::Result<Response, ServerError> {
5158    let Json(request) = request
5159        .map_err(|e| ServerError::invalid_request(format!("invalid speech request: {e}"), None))?;
5160
5161    let response_format = speech_output_format(&request)?;
5162    resolve_request_model(
5163        &state.served_model_registry,
5164        &request.model,
5165        ServedModelKind::Speech,
5166    )?;
5167
5168    let span = span!(Level::INFO, "speech");
5169    let _guard = span.enter();
5170
5171    let language = if request.language.is_empty() || request.language == "auto" {
5172        None
5173    } else {
5174        Some(request.language.as_str())
5175    };
5176
5177    let chunk_frames = 10usize;
5178    let tts = state.tts.as_ref().ok_or_else(|| {
5179        ServerError::NotImplemented("TTS engine not loaded; speech unavailable".into())
5180    })?;
5181    let sample_rate = tts.tts_sample_rate();
5182
5183    if request.stream {
5184        // Streaming: chunked transfer encoding with WAV audio
5185        let (tx, rx) =
5186            mpsc::unbounded_channel::<std::result::Result<axum::body::Bytes, std::io::Error>>();
5187
5188        let engine = tts.clone();
5189        let text = request.input.clone();
5190        let lang = request.language.clone();
5191
5192        tokio::task::spawn_blocking(move || {
5193            let lang_opt = if lang.is_empty() || lang == "auto" {
5194                None
5195            } else {
5196                Some(lang.as_str())
5197            };
5198            let rt = tokio::runtime::Handle::current();
5199
5200            match rt.block_on(engine.synthesize_speech(&text, lang_opt, chunk_frames)) {
5201                Ok(chunks) => {
5202                    for chunk in &chunks {
5203                        let audio_bytes = encode_speech_audio(chunk, sample_rate, response_format);
5204                        let _ = tx.send(Ok(axum::body::Bytes::from(audio_bytes)));
5205                    }
5206                }
5207                Err(e) => {
5208                    error!("TTS error: {e}");
5209                }
5210            }
5211        });
5212
5213        let stream = tokio_stream::wrappers::UnboundedReceiverStream::new(rx);
5214        let body = axum::body::Body::from_stream(stream);
5215        Ok(Response::builder()
5216            .status(200)
5217            .header("content-type", speech_content_type(response_format))
5218            .header("transfer-encoding", "chunked")
5219            .body(body)
5220            .unwrap())
5221    } else {
5222        // Non-streaming: return complete WAV
5223        let chunks = tts
5224            .synthesize_speech(&request.input, language, chunk_frames)
5225            .await
5226            .map_err(|e| ServerError::InternalError(format!("TTS: {e}")))?;
5227
5228        let all_samples: Vec<f32> = chunks.into_iter().flatten().collect();
5229        let audio_bytes = encode_speech_audio(&all_samples, sample_rate, response_format);
5230
5231        Ok(Response::builder()
5232            .status(200)
5233            .header("content-type", speech_content_type(response_format))
5234            .header("content-length", audio_bytes.len().to_string())
5235            .body(axum::body::Body::from(audio_bytes))
5236            .unwrap())
5237    }
5238}
5239
5240#[derive(Clone, Copy)]
5241enum SpeechOutputFormat {
5242    Wav,
5243    Pcm,
5244}
5245
5246fn speech_output_format(
5247    request: &SpeechRequest,
5248) -> std::result::Result<SpeechOutputFormat, ServerError> {
5249    if request.response_format.eq_ignore_ascii_case("wav") {
5250        Ok(SpeechOutputFormat::Wav)
5251    } else if request.response_format.eq_ignore_ascii_case("pcm") {
5252        Ok(SpeechOutputFormat::Pcm)
5253    } else {
5254        Err(ServerError::unsupported_feature(
5255            "only response_format=wav or response_format=pcm is supported for speech",
5256            Some("response_format"),
5257        ))
5258    }
5259}
5260
5261fn speech_content_type(format: SpeechOutputFormat) -> &'static str {
5262    match format {
5263        SpeechOutputFormat::Wav => "audio/wav",
5264        SpeechOutputFormat::Pcm => "audio/pcm",
5265    }
5266}
5267
5268fn encode_speech_audio(samples: &[f32], sample_rate: u32, format: SpeechOutputFormat) -> Vec<u8> {
5269    match format {
5270        SpeechOutputFormat::Wav => pcm_to_wav_bytes(samples, sample_rate),
5271        SpeechOutputFormat::Pcm => pcm_to_s16le_bytes(samples),
5272    }
5273}
5274
5275/// Convert PCM f32 samples to WAV bytes (16-bit, mono).
5276fn pcm_to_wav_bytes(samples: &[f32], sample_rate: u32) -> Vec<u8> {
5277    let num_samples = samples.len();
5278    let data_size = num_samples * 2; // 16-bit = 2 bytes per sample
5279    let file_size = 44 + data_size;
5280
5281    let mut buf = Vec::with_capacity(file_size);
5282    // RIFF header
5283    buf.extend_from_slice(b"RIFF");
5284    buf.extend_from_slice(&((file_size - 8) as u32).to_le_bytes());
5285    buf.extend_from_slice(b"WAVE");
5286    // fmt chunk
5287    buf.extend_from_slice(b"fmt ");
5288    buf.extend_from_slice(&16u32.to_le_bytes()); // chunk size
5289    buf.extend_from_slice(&1u16.to_le_bytes()); // PCM
5290    buf.extend_from_slice(&1u16.to_le_bytes()); // mono
5291    buf.extend_from_slice(&sample_rate.to_le_bytes());
5292    buf.extend_from_slice(&(sample_rate * 2).to_le_bytes()); // byte rate
5293    buf.extend_from_slice(&2u16.to_le_bytes()); // block align
5294    buf.extend_from_slice(&16u16.to_le_bytes()); // bits per sample
5295                                                 // data chunk
5296    buf.extend_from_slice(b"data");
5297    buf.extend_from_slice(&(data_size as u32).to_le_bytes());
5298    buf.extend_from_slice(&pcm_to_s16le_bytes(samples));
5299    buf
5300}
5301
5302fn pcm_to_s16le_bytes(samples: &[f32]) -> Vec<u8> {
5303    let mut buf = Vec::with_capacity(samples.len() * 2);
5304    for &s in samples {
5305        let i16_val = (s.clamp(-1.0, 1.0) * 32767.0) as i16;
5306        buf.extend_from_slice(&i16_val.to_le_bytes());
5307    }
5308    buf
5309}
5310
5311async fn models_handler(
5312    State(state): State<AppState>,
5313) -> std::result::Result<Response, ServerError> {
5314    let now = chrono::Utc::now().timestamp() as u64;
5315    let data = state
5316        .served_model_registry
5317        .entries()
5318        .iter()
5319        .map(|entry| crate::openai::ModelInfo {
5320            id: entry.public_name().to_string(),
5321            object: "model".to_string(),
5322            created: now,
5323            owned_by: "ferrum".to_string(),
5324            modalities: entry
5325                .kind()
5326                .modalities()
5327                .iter()
5328                .map(ToString::to_string)
5329                .collect(),
5330            permission: vec![],
5331            root: entry.parent_public_name().map(ToString::to_string),
5332            parent: entry.parent_public_name().map(ToString::to_string),
5333        })
5334        .collect();
5335
5336    let models = ModelListResponse {
5337        object: "list".to_string(),
5338        data,
5339    };
5340
5341    Ok(Json(models).into_response())
5342}
5343
5344async fn health_handler(
5345    State(state): State<AppState>,
5346) -> std::result::Result<Response, ServerError> {
5347    let engine_status = state.status().await;
5348    let scheduler_metrics = state.metrics();
5349    let runtime_config = RuntimeConfigSnapshot::capture_current();
5350    let cache_policy = CachePolicy::current();
5351    let engine_cache = state
5352        .llm
5353        .as_ref()
5354        .and_then(|engine| engine.cache_metrics_snapshot());
5355    let execution_attribution = state
5356        .llm
5357        .as_ref()
5358        .and_then(|engine| engine.execution_attribution_snapshot());
5359    let engine_lora = state
5360        .llm
5361        .as_ref()
5362        .and_then(|engine| engine.lora_metrics_snapshot());
5363    let auto_config = auto_config_health_value(state.auto_config.as_ref());
5364    let runtime_admission = match state.llm.as_ref() {
5365        Some(engine) => engine.admission_snapshot(),
5366        None => Ok(None),
5367    };
5368    let (runtime_admission_snapshot, runtime_admission_error) = match &runtime_admission {
5369        Ok(snapshot) => (snapshot.as_ref(), None),
5370        Err(error) => (None, Some(error.to_string())),
5371    };
5372    let admission = admission_health_json(
5373        &engine_status,
5374        &scheduler_metrics,
5375        &auto_config,
5376        runtime_admission_snapshot,
5377        runtime_admission_error.as_deref(),
5378    );
5379
5380    let health = serde_json::json!({
5381        "status": if runtime_admission_error.is_some() { "unhealthy" } else { "healthy" },
5382        "reasoning_protocol": state.prompt_template.as_deref().map(ModelChatTemplate::reasoning_capability).unwrap_or_default(),
5383        "timestamp": chrono::Utc::now().to_rfc3339(),
5384        "version": env!("CARGO_PKG_VERSION"),
5385        "engine": {
5386            "active_requests": engine_status.active_requests,
5387            "queued_requests": engine_status.queued_requests,
5388        },
5389        "scheduler": {
5390            "total_requests": scheduler_metrics.total_requests,
5391            "successful_requests": scheduler_metrics.successful_requests,
5392            "failed_requests": scheduler_metrics.failed_requests,
5393            "throughput_rps": scheduler_metrics.throughput_rps,
5394            "avg_wait_time_ms": scheduler_metrics.queue_metrics.avg_queue_wait_time_ms,
5395            "scheduling_time_ms": scheduler_metrics.performance_breakdown.scheduling_time_ms,
5396            "model_execution_time_ms": scheduler_metrics
5397                .performance_breakdown
5398                .model_execution_time_ms,
5399            "iteration_lock_wait_time_ms": scheduler_metrics
5400                .performance_breakdown
5401                .other_overhead_time_ms,
5402        },
5403        "config": runtime_config,
5404        "auto_config": auto_config,
5405        "admission": admission,
5406        "cache": state.cache.health_json(&cache_policy, engine_cache.as_ref()),
5407        "execution_attribution": execution_attribution,
5408        "lora": engine_lora.unwrap_or_else(|| serde_json::json!({
5409            "enabled": state.served_model_registry.adapter_count() > 0,
5410            "adapter_count": state.served_model_registry.adapter_count() as u64,
5411            "active_cache_bindings": 0u64,
5412            "projection_applications": 0u64,
5413            "position": "startup-routing",
5414            "source": "server-lora-registry",
5415        })),
5416    });
5417
5418    Ok(Json(health).into_response())
5419}
5420
5421/// Prometheus metrics endpoint — returns metrics in Prometheus text format.
5422async fn metrics_handler(
5423    State(state): State<AppState>,
5424) -> std::result::Result<Response, ServerError> {
5425    let mut body = match PROM_HANDLE.get() {
5426        Some(handle) => handle.render(),
5427        None => "# Prometheus recorder not initialized\n".to_string(),
5428    };
5429    if !body.ends_with('\n') {
5430        body.push('\n');
5431    }
5432    let engine_cache = state
5433        .llm
5434        .as_ref()
5435        .and_then(|engine| engine.cache_metrics_snapshot());
5436    body.push_str(&state.cache.prometheus_metrics(engine_cache.as_ref()));
5437    let engine_status = state.status().await;
5438    let scheduler_metrics = state.metrics();
5439    let auto_config = auto_config_health_value(state.auto_config.as_ref());
5440    let runtime_admission = match state.llm.as_ref() {
5441        Some(engine) => engine.admission_snapshot(),
5442        None => Ok(None),
5443    };
5444    let (runtime_admission_snapshot, runtime_admission_error) = match &runtime_admission {
5445        Ok(snapshot) => (snapshot.as_ref(), None),
5446        Err(error) => (None, Some(error.to_string())),
5447    };
5448    let admission = admission_health_json(
5449        &engine_status,
5450        &scheduler_metrics,
5451        &auto_config,
5452        runtime_admission_snapshot,
5453        runtime_admission_error.as_deref(),
5454    );
5455    body.push_str(&admission_prometheus_metrics(&admission));
5456
5457    Ok((
5458        [(
5459            axum::http::header::CONTENT_TYPE,
5460            "text/plain; version=0.0.4; charset=utf-8",
5461        )],
5462        body,
5463    )
5464        .into_response())
5465}
5466
5467async fn root_handler() -> std::result::Result<Response, ServerError> {
5468    let info = serde_json::json!({
5469        "name": "Ferrum Inference Server",
5470        "version": env!("CARGO_PKG_VERSION"),
5471        "api_version": "v1",
5472        "status": "running"
5473    });
5474
5475    Ok(Json(info).into_response())
5476}
5477
5478/// Server error type for HTTP responses
5479#[derive(Debug)]
5480enum ServerError {
5481    InvalidRequest {
5482        message: String,
5483        param: Option<String>,
5484    },
5485    UnsupportedFeature {
5486        message: String,
5487        param: Option<String>,
5488    },
5489    InternalError(String),
5490    NotImplemented(String),
5491    ServiceUnavailable(String),
5492}
5493
5494impl ServerError {
5495    fn invalid_request(message: impl Into<String>, param: Option<&str>) -> Self {
5496        Self::InvalidRequest {
5497            message: message.into(),
5498            param: param.map(str::to_string),
5499        }
5500    }
5501
5502    fn unsupported_feature(message: impl Into<String>, param: Option<&str>) -> Self {
5503        Self::UnsupportedFeature {
5504            message: message.into(),
5505            param: param.map(str::to_string),
5506        }
5507    }
5508}
5509
5510impl IntoResponse for ServerError {
5511    fn into_response(self) -> Response {
5512        let (status, message, error_type, param) = match self {
5513            ServerError::InvalidRequest { message, param } => (
5514                AxumStatusCode::BAD_REQUEST,
5515                message,
5516                "invalid_request_error",
5517                param,
5518            ),
5519            ServerError::UnsupportedFeature { message, param } => (
5520                AxumStatusCode::BAD_REQUEST,
5521                message,
5522                "invalid_request_error",
5523                param,
5524            ),
5525            ServerError::InternalError(msg) => (
5526                AxumStatusCode::INTERNAL_SERVER_ERROR,
5527                msg,
5528                "internal_server_error",
5529                None,
5530            ),
5531            ServerError::NotImplemented(msg) => (
5532                AxumStatusCode::SERVICE_UNAVAILABLE,
5533                msg,
5534                "service_unavailable_error",
5535                None,
5536            ),
5537            ServerError::ServiceUnavailable(msg) => (
5538                AxumStatusCode::SERVICE_UNAVAILABLE,
5539                msg,
5540                "service_unavailable_error",
5541                None,
5542            ),
5543        };
5544
5545        let error = OpenAiError {
5546            error: OpenAiErrorDetail {
5547                message,
5548                error_type: error_type.to_string(),
5549                param,
5550                code: None,
5551            },
5552        };
5553
5554        (status, Json(error)).into_response()
5555    }
5556}
5557
5558impl std::fmt::Display for MessageRole {
5559    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5560        match self {
5561            MessageRole::System => write!(f, "system"),
5562            MessageRole::User => write!(f, "user"),
5563            MessageRole::Assistant => write!(f, "assistant"),
5564            MessageRole::Function => write!(f, "function"),
5565            MessageRole::Tool => write!(f, "tool"),
5566        }
5567    }
5568}
5569
5570/// Strip model output at the first user-supplied stop sequence.
5571/// OpenAI-compatible `stop` strings are generation boundaries and must not
5572/// be returned to the caller, even if the model continued after the boundary.
5573fn strip_after_stop(text: &str, stops: &[String]) -> String {
5574    let mut first: Option<usize> = None;
5575    for stop in stops {
5576        if stop.is_empty() {
5577            continue;
5578        }
5579        if let Some(idx) = text.find(stop.as_str()) {
5580            first = Some(first.map_or(idx, |current| current.min(idx)));
5581        }
5582    }
5583    match first {
5584        Some(idx) => text[..idx].to_string(),
5585        None => text.to_string(),
5586    }
5587}
5588
5589/// Compatibility cleanup for explicitly best-effort structured output.
5590/// Hard `json_object` and strict schema contracts never call this helper.
5591fn strip_markdown_json_fence(text: &str) -> String {
5592    let trimmed = text.trim();
5593    // Try the most specific marker first.
5594    for prefix in ["```json\n", "```json", "```\n", "```"] {
5595        if let Some(rest) = trimmed.strip_prefix(prefix) {
5596            if let Some(inner) = rest.strip_suffix("```") {
5597                return inner.trim().to_string();
5598            }
5599        }
5600    }
5601    text.to_string()
5602}
5603
5604/// Convert FinishReason to OpenAI API string
5605fn finish_reason_to_string(reason: &FinishReason) -> String {
5606    match reason {
5607        FinishReason::Length => "length".to_string(),
5608        FinishReason::Stop => "stop".to_string(),
5609        FinishReason::EOS => "stop".to_string(),
5610        FinishReason::Cancelled => "cancelled".to_string(),
5611        FinishReason::Error => "error".to_string(),
5612        FinishReason::ContentFilter => "content_filter".to_string(),
5613    }
5614}
5615
5616#[cfg(test)]
5617mod tests {
5618    mod engine_stop_contract;
5619    mod gemma_thought;
5620    mod harmony_stops;
5621    use super::*;
5622    use async_trait::async_trait;
5623    use axum::{
5624        body::{to_bytes, Body},
5625        http::{header, Request},
5626        response::Response,
5627    };
5628    use ferrum_interfaces::engine::{
5629        EmbedEngine, InferenceEngine, LlmInferenceEngine, TranscribeEngine, TtsEngine,
5630    };
5631    use ferrum_types::{
5632        has_unclosed_thinking_block, parse_reasoning_response_started_in_think, EngineConfig,
5633        EngineMetrics, EngineStatus, EngineTokenTimingEvidence, FinishReason,
5634        HealthStatus as EngineHealthStatus, InferenceRequest, InferenceResponse, MemoryUsage,
5635        ModelId, StreamChunk, TokenId, TokenUsage,
5636    };
5637    use futures::{stream, Stream};
5638    use serde_json::{json, Value};
5639    use std::{
5640        collections::HashMap,
5641        pin::Pin,
5642        sync::{atomic::AtomicUsize, Arc, Mutex},
5643    };
5644    use tower::ServiceExt;
5645
5646    #[test]
5647    fn strip_after_stop_removes_first_boundary() {
5648        assert_eq!(
5649            strip_after_stop(
5650                "KS0214Z\nS0225\nEND0214Z0214Z\nS0225\n",
5651                &["END0214Z".to_string()]
5652            ),
5653            "KS0214Z\nS0225\n"
5654        );
5655    }
5656
5657    #[test]
5658    fn gpt_oss_harmony_final_is_split_into_reasoning_and_visible_content() {
5659        let parsed = parse_chat_model_output(
5660            ModelOutputProtocol::HarmonyGptOss,
5661            "<|channel|>analysis<|message|>Reason.<|end|>\
5662             <|start|>assistant<|channel|>final<|message|>Answer.<|return|>",
5663            false,
5664            FinishReason::Stop,
5665        )
5666        .unwrap();
5667        assert_eq!(parsed.visible.content, "Answer.");
5668        assert_eq!(parsed.visible.reasoning.as_deref(), Some("Reason."));
5669        assert!(parsed.harmony_response.is_none());
5670    }
5671
5672    #[test]
5673    fn gpt_oss_harmony_tool_call_becomes_openai_structured_response() {
5674        let parsed = parse_chat_model_output(
5675            ModelOutputProtocol::HarmonyGptOss,
5676            "<|channel|>analysis<|message|>Need weather.<|end|>\
5677             <|start|>assistant<|channel|>commentary to=functions.weather\
5678             <|constrain|>json<|message|>{\"city\":\"Paris\"}<|call|>",
5679            false,
5680            FinishReason::Stop,
5681        )
5682        .unwrap();
5683        let response = parsed.harmony_response.unwrap();
5684        assert_eq!(response.finish_reason.as_deref(), Some("tool_calls"));
5685        assert_eq!(response.message.tool_calls.len(), 1);
5686        assert_eq!(response.message.tool_calls[0].function.name, "weather");
5687        assert_eq!(
5688            response.message.tool_calls[0].function.arguments,
5689            "{\"city\":\"Paris\"}"
5690        );
5691        assert!(response.message.tool_calls[0].id.starts_with("call_"));
5692    }
5693
5694    #[test]
5695    fn gpt_oss_harmony_accepts_missing_text_terminal_only_for_explicit_truncation() {
5696        let output = "<|channel|>analysis<|message|>Still reasoning";
5697        for finish_reason in [FinishReason::Stop, FinishReason::Length] {
5698            let parsed = parse_chat_model_output(
5699                ModelOutputProtocol::HarmonyGptOss,
5700                output,
5701                false,
5702                finish_reason,
5703            )
5704            .unwrap();
5705            assert_eq!(parsed.visible.reasoning.as_deref(), Some("Still reasoning"));
5706            assert!(parsed.visible.content.is_empty());
5707        }
5708        for finish_reason in [
5709            FinishReason::EOS,
5710            FinishReason::Cancelled,
5711            FinishReason::Error,
5712            FinishReason::ContentFilter,
5713        ] {
5714            assert!(parse_chat_model_output(
5715                ModelOutputProtocol::HarmonyGptOss,
5716                output,
5717                false,
5718                finish_reason,
5719            )
5720            .is_err());
5721        }
5722    }
5723
5724    #[tokio::test]
5725    async fn stop_drains_running_server_and_shuts_down_loaded_engine_once() {
5726        let engine = Arc::new(StubLlm::new("ok"));
5727        let server = Arc::new(AxumServer::from_llm(engine.clone()));
5728        let config = ServerConfig {
5729            host: "127.0.0.1".to_string(),
5730            port: 0,
5731            ..ServerConfig::default()
5732        };
5733        let server_task = {
5734            let server = Arc::clone(&server);
5735            tokio::spawn(async move { server.start(&config).await })
5736        };
5737        tokio::time::timeout(std::time::Duration::from_secs(1), async {
5738            while !server.is_running() {
5739                tokio::task::yield_now().await;
5740            }
5741        })
5742        .await
5743        .unwrap();
5744
5745        server
5746            .stop(std::time::Duration::from_secs(1))
5747            .await
5748            .unwrap();
5749        server
5750            .stop(std::time::Duration::from_secs(1))
5751            .await
5752            .unwrap();
5753        server_task.await.unwrap().unwrap();
5754
5755        assert_eq!(engine.shutdown_count.load(Ordering::Acquire), 1);
5756        assert!(!server.is_running());
5757    }
5758
5759    struct StubLlm {
5760        config: EngineConfig,
5761        text: String,
5762        stream_chunks: Option<Vec<String>>,
5763        stream_final_chunk_separate: bool,
5764        stream_tail_without_token: bool,
5765        stream_usage: Option<TokenUsage>,
5766        api_response: Option<ferrum_types::ApiResponse>,
5767        finish_reason: FinishReason,
5768        execution_attribution: Option<Value>,
5769        lora_metrics: Option<Value>,
5770        pending_stream_drop_notify: Option<Arc<Notify>>,
5771        shutdown_count: AtomicUsize,
5772    }
5773
5774    impl StubLlm {
5775        fn new(text: &str) -> Self {
5776            let mut config = EngineConfig::default();
5777            config.model.model_id = ModelId::new("stub-model");
5778            Self {
5779                config,
5780                text: text.to_string(),
5781                stream_chunks: None,
5782                stream_final_chunk_separate: false,
5783                stream_tail_without_token: false,
5784                stream_usage: Some(TokenUsage::new(5, 1)),
5785                api_response: None,
5786                finish_reason: FinishReason::EOS,
5787                execution_attribution: None,
5788                lora_metrics: None,
5789                pending_stream_drop_notify: None,
5790                shutdown_count: AtomicUsize::new(0),
5791            }
5792        }
5793
5794        fn without_stream_usage(text: &str) -> Self {
5795            Self {
5796                stream_usage: None,
5797                ..Self::new(text)
5798            }
5799        }
5800
5801        fn with_stream_chunks(chunks: &[&str]) -> Self {
5802            Self {
5803                text: chunks.concat(),
5804                stream_chunks: Some(chunks.iter().map(|chunk| (*chunk).to_string()).collect()),
5805                stream_usage: Some(TokenUsage::new(5, chunks.len())),
5806                ..Self::new("")
5807            }
5808        }
5809
5810        fn with_separate_final_stream_chunk(chunks: &[&str]) -> Self {
5811            Self {
5812                text: chunks.concat(),
5813                stream_chunks: Some(chunks.iter().map(|chunk| (*chunk).to_string()).collect()),
5814                stream_final_chunk_separate: true,
5815                stream_usage: Some(TokenUsage::new(5, chunks.len())),
5816                ..Self::new("")
5817            }
5818        }
5819
5820        fn with_tokenless_tail(chunks: &[&str]) -> Self {
5821            Self {
5822                stream_tail_without_token: true,
5823                ..Self::with_separate_final_stream_chunk(chunks)
5824            }
5825        }
5826
5827        fn with_api_response(text: &str, api_response: ferrum_types::ApiResponse) -> Self {
5828            Self {
5829                api_response: Some(api_response),
5830                ..Self::new(text)
5831            }
5832        }
5833
5834        fn with_api_response_and_finish_reason(
5835            text: &str,
5836            api_response: ferrum_types::ApiResponse,
5837            finish_reason: FinishReason,
5838        ) -> Self {
5839            Self {
5840                api_response: Some(api_response),
5841                finish_reason,
5842                ..Self::new(text)
5843            }
5844        }
5845
5846        fn with_lora_metrics(text: &str, lora_metrics: Value) -> Self {
5847            Self {
5848                lora_metrics: Some(lora_metrics),
5849                ..Self::new(text)
5850            }
5851        }
5852
5853        fn with_execution_attribution(text: &str, execution_attribution: Value) -> Self {
5854            Self {
5855                execution_attribution: Some(execution_attribution),
5856                ..Self::new(text)
5857            }
5858        }
5859
5860        fn with_pending_stream(drop_notify: Arc<Notify>) -> Self {
5861            Self {
5862                pending_stream_drop_notify: Some(drop_notify),
5863                ..Self::new("")
5864            }
5865        }
5866    }
5867
5868    struct PendingDropStream {
5869        drop_notify: Arc<Notify>,
5870    }
5871
5872    impl Stream for PendingDropStream {
5873        type Item = ferrum_types::Result<StreamChunk>;
5874
5875        fn poll_next(
5876            self: Pin<&mut Self>,
5877            _cx: &mut std::task::Context<'_>,
5878        ) -> std::task::Poll<Option<Self::Item>> {
5879            std::task::Poll::Pending
5880        }
5881    }
5882
5883    impl Drop for PendingDropStream {
5884        fn drop(&mut self) {
5885            self.drop_notify.notify_one();
5886        }
5887    }
5888
5889    struct StubEmbed {
5890        config: EngineConfig,
5891    }
5892
5893    impl StubEmbed {
5894        fn new() -> Self {
5895            let mut config = EngineConfig::default();
5896            config.model.model_id = ModelId::new("stub-embed");
5897            Self { config }
5898        }
5899    }
5900
5901    struct StubTranscribe {
5902        config: EngineConfig,
5903    }
5904
5905    impl StubTranscribe {
5906        fn new() -> Self {
5907            let mut config = EngineConfig::default();
5908            config.model.model_id = ModelId::new("stub-transcribe");
5909            Self { config }
5910        }
5911    }
5912
5913    struct StubTts {
5914        config: EngineConfig,
5915    }
5916
5917    impl StubTts {
5918        fn new() -> Self {
5919            let mut config = EngineConfig::default();
5920            config.model.model_id = ModelId::new("stub-tts");
5921            Self { config }
5922        }
5923    }
5924
5925    struct FailingLlm {
5926        config: EngineConfig,
5927        fail_after_stream_start: bool,
5928        infer_failure: ferrum_types::FerrumError,
5929        stream_start_failure: ferrum_types::FerrumError,
5930        stream_chunk_failure: ferrum_types::FerrumError,
5931    }
5932
5933    impl FailingLlm {
5934        fn new() -> Self {
5935            let mut config = EngineConfig::default();
5936            config.model.model_id = ModelId::new("failing-model");
5937            Self {
5938                config,
5939                fail_after_stream_start: false,
5940                infer_failure: ferrum_types::FerrumError::internal("stub generation failed"),
5941                stream_start_failure: ferrum_types::FerrumError::internal("stub stream failed"),
5942                stream_chunk_failure: ferrum_types::FerrumError::internal(
5943                    "stub stream chunk failed",
5944                ),
5945            }
5946        }
5947
5948        fn after_stream_start() -> Self {
5949            Self {
5950                fail_after_stream_start: true,
5951                ..Self::new()
5952            }
5953        }
5954
5955        fn resource_exhausted() -> Self {
5956            let failure = ferrum_types::FerrumError::resource_exhausted(
5957                "admission capacity exhausted while reserving request resources",
5958            );
5959            Self {
5960                infer_failure: failure.clone(),
5961                stream_start_failure: failure.clone(),
5962                stream_chunk_failure: failure,
5963                ..Self::new()
5964            }
5965        }
5966    }
5967
5968    struct CapturingLlm {
5969        config: EngineConfig,
5970        last_request: Mutex<Option<InferenceRequest>>,
5971    }
5972
5973    impl CapturingLlm {
5974        fn new() -> Self {
5975            let mut config = EngineConfig::default();
5976            config.model.model_id = ModelId::new("qwen3");
5977            Self {
5978                config,
5979                last_request: Mutex::new(None),
5980            }
5981        }
5982
5983        fn last_request(&self) -> InferenceRequest {
5984            self.last_request
5985                .lock()
5986                .expect("capture lock")
5987                .clone()
5988                .expect("request captured")
5989        }
5990
5991        fn has_captured_request(&self) -> bool {
5992            self.last_request.lock().expect("capture lock").is_some()
5993        }
5994    }
5995
5996    #[async_trait]
5997    impl InferenceEngine for StubLlm {
5998        async fn status(&self) -> EngineStatus {
5999            EngineStatus {
6000                is_ready: true,
6001                loaded_models: vec![self.config.model.model_id.clone()],
6002                active_requests: 0,
6003                queued_requests: 0,
6004                memory_usage: MemoryUsage {
6005                    total_bytes: 0,
6006                    used_bytes: 0,
6007                    free_bytes: 0,
6008                    gpu_memory_bytes: None,
6009                    cpu_memory_bytes: None,
6010                    cache_memory_bytes: 0,
6011                    utilization_percent: 0.0,
6012                },
6013                uptime_seconds: 0,
6014                last_heartbeat: chrono::Utc::now(),
6015                version: "test".to_string(),
6016            }
6017        }
6018
6019        async fn shutdown(&self) -> ferrum_types::Result<()> {
6020            self.shutdown_count.fetch_add(1, Ordering::AcqRel);
6021            Ok(())
6022        }
6023
6024        fn config(&self) -> &EngineConfig {
6025            &self.config
6026        }
6027
6028        fn metrics(&self) -> EngineMetrics {
6029            EngineMetrics::default()
6030        }
6031
6032        async fn health_check(&self) -> EngineHealthStatus {
6033            EngineHealthStatus::healthy()
6034        }
6035
6036        fn execution_attribution_snapshot(&self) -> Option<Value> {
6037            self.execution_attribution.clone()
6038        }
6039
6040        fn lora_metrics_snapshot(&self) -> Option<Value> {
6041            self.lora_metrics.clone()
6042        }
6043    }
6044
6045    #[async_trait]
6046    impl InferenceEngine for StubEmbed {
6047        async fn status(&self) -> EngineStatus {
6048            EngineStatus {
6049                is_ready: true,
6050                loaded_models: vec![self.config.model.model_id.clone()],
6051                active_requests: 0,
6052                queued_requests: 0,
6053                memory_usage: MemoryUsage {
6054                    total_bytes: 0,
6055                    used_bytes: 0,
6056                    free_bytes: 0,
6057                    gpu_memory_bytes: None,
6058                    cpu_memory_bytes: None,
6059                    cache_memory_bytes: 0,
6060                    utilization_percent: 0.0,
6061                },
6062                uptime_seconds: 0,
6063                last_heartbeat: chrono::Utc::now(),
6064                version: "test".to_string(),
6065            }
6066        }
6067
6068        async fn shutdown(&self) -> ferrum_types::Result<()> {
6069            Ok(())
6070        }
6071
6072        fn config(&self) -> &EngineConfig {
6073            &self.config
6074        }
6075
6076        fn metrics(&self) -> EngineMetrics {
6077            EngineMetrics::default()
6078        }
6079
6080        async fn health_check(&self) -> EngineHealthStatus {
6081            EngineHealthStatus::healthy()
6082        }
6083    }
6084
6085    #[async_trait]
6086    impl EmbedEngine for StubEmbed {
6087        async fn embed_text(&self, text: &str) -> ferrum_types::Result<Vec<f32>> {
6088            Ok(vec![text.len() as f32, 1.0, 0.0])
6089        }
6090
6091        async fn embed_image(&self, image: &str) -> ferrum_types::Result<Vec<f32>> {
6092            Ok(vec![image.len() as f32, 0.0, 1.0])
6093        }
6094
6095        fn embedding_dim(&self) -> usize {
6096            3
6097        }
6098    }
6099
6100    #[async_trait]
6101    impl InferenceEngine for StubTranscribe {
6102        async fn status(&self) -> EngineStatus {
6103            EngineStatus {
6104                is_ready: true,
6105                loaded_models: vec![self.config.model.model_id.clone()],
6106                active_requests: 0,
6107                queued_requests: 0,
6108                memory_usage: MemoryUsage {
6109                    total_bytes: 0,
6110                    used_bytes: 0,
6111                    free_bytes: 0,
6112                    gpu_memory_bytes: None,
6113                    cpu_memory_bytes: None,
6114                    cache_memory_bytes: 0,
6115                    utilization_percent: 0.0,
6116                },
6117                uptime_seconds: 0,
6118                last_heartbeat: chrono::Utc::now(),
6119                version: "test".to_string(),
6120            }
6121        }
6122
6123        async fn shutdown(&self) -> ferrum_types::Result<()> {
6124            Ok(())
6125        }
6126
6127        fn config(&self) -> &EngineConfig {
6128            &self.config
6129        }
6130
6131        fn metrics(&self) -> EngineMetrics {
6132            EngineMetrics::default()
6133        }
6134
6135        async fn health_check(&self) -> EngineHealthStatus {
6136            EngineHealthStatus::healthy()
6137        }
6138    }
6139
6140    #[async_trait]
6141    impl TranscribeEngine for StubTranscribe {
6142        async fn transcribe_file(
6143            &self,
6144            path: &str,
6145            language: Option<&str>,
6146        ) -> ferrum_types::Result<String> {
6147            Ok(format!("file:{path}:{}", language.unwrap_or("auto")))
6148        }
6149
6150        async fn transcribe_bytes(
6151            &self,
6152            data: &[u8],
6153            language: Option<&str>,
6154        ) -> ferrum_types::Result<String> {
6155            Ok(format!(
6156                "bytes:{}:{}",
6157                data.len(),
6158                language.unwrap_or("auto")
6159            ))
6160        }
6161    }
6162
6163    #[async_trait]
6164    impl InferenceEngine for StubTts {
6165        async fn status(&self) -> EngineStatus {
6166            EngineStatus {
6167                is_ready: true,
6168                loaded_models: vec![self.config.model.model_id.clone()],
6169                active_requests: 0,
6170                queued_requests: 0,
6171                memory_usage: MemoryUsage {
6172                    total_bytes: 0,
6173                    used_bytes: 0,
6174                    free_bytes: 0,
6175                    gpu_memory_bytes: None,
6176                    cpu_memory_bytes: None,
6177                    cache_memory_bytes: 0,
6178                    utilization_percent: 0.0,
6179                },
6180                uptime_seconds: 0,
6181                last_heartbeat: chrono::Utc::now(),
6182                version: "test".to_string(),
6183            }
6184        }
6185
6186        async fn shutdown(&self) -> ferrum_types::Result<()> {
6187            Ok(())
6188        }
6189
6190        fn config(&self) -> &EngineConfig {
6191            &self.config
6192        }
6193
6194        fn metrics(&self) -> EngineMetrics {
6195            EngineMetrics::default()
6196        }
6197
6198        async fn health_check(&self) -> EngineHealthStatus {
6199            EngineHealthStatus::healthy()
6200        }
6201    }
6202
6203    #[async_trait]
6204    impl TtsEngine for StubTts {
6205        async fn synthesize_speech(
6206            &self,
6207            _text: &str,
6208            _language: Option<&str>,
6209            _chunk_frames: usize,
6210        ) -> ferrum_types::Result<Vec<Vec<f32>>> {
6211            Ok(vec![vec![0.0, 0.5, -0.5]])
6212        }
6213
6214        fn tts_sample_rate(&self) -> u32 {
6215            16_000
6216        }
6217    }
6218
6219    #[async_trait]
6220    impl InferenceEngine for FailingLlm {
6221        async fn status(&self) -> EngineStatus {
6222            EngineStatus {
6223                is_ready: true,
6224                loaded_models: vec![self.config.model.model_id.clone()],
6225                active_requests: 0,
6226                queued_requests: 0,
6227                memory_usage: MemoryUsage {
6228                    total_bytes: 0,
6229                    used_bytes: 0,
6230                    free_bytes: 0,
6231                    gpu_memory_bytes: None,
6232                    cpu_memory_bytes: None,
6233                    cache_memory_bytes: 0,
6234                    utilization_percent: 0.0,
6235                },
6236                uptime_seconds: 0,
6237                last_heartbeat: chrono::Utc::now(),
6238                version: "test".to_string(),
6239            }
6240        }
6241
6242        async fn shutdown(&self) -> ferrum_types::Result<()> {
6243            Ok(())
6244        }
6245
6246        fn config(&self) -> &EngineConfig {
6247            &self.config
6248        }
6249
6250        fn metrics(&self) -> EngineMetrics {
6251            EngineMetrics::default()
6252        }
6253
6254        async fn health_check(&self) -> EngineHealthStatus {
6255            EngineHealthStatus::healthy()
6256        }
6257    }
6258
6259    #[async_trait]
6260    impl InferenceEngine for CapturingLlm {
6261        async fn status(&self) -> EngineStatus {
6262            EngineStatus {
6263                is_ready: true,
6264                loaded_models: vec![self.config.model.model_id.clone()],
6265                active_requests: 0,
6266                queued_requests: 0,
6267                memory_usage: MemoryUsage {
6268                    total_bytes: 0,
6269                    used_bytes: 0,
6270                    free_bytes: 0,
6271                    gpu_memory_bytes: None,
6272                    cpu_memory_bytes: None,
6273                    cache_memory_bytes: 0,
6274                    utilization_percent: 0.0,
6275                },
6276                uptime_seconds: 0,
6277                last_heartbeat: chrono::Utc::now(),
6278                version: "test".to_string(),
6279            }
6280        }
6281
6282        async fn shutdown(&self) -> ferrum_types::Result<()> {
6283            Ok(())
6284        }
6285
6286        fn config(&self) -> &EngineConfig {
6287            &self.config
6288        }
6289
6290        fn metrics(&self) -> EngineMetrics {
6291            EngineMetrics::default()
6292        }
6293
6294        async fn health_check(&self) -> EngineHealthStatus {
6295            EngineHealthStatus::healthy()
6296        }
6297    }
6298
6299    fn stub_execution_evidence(
6300        request: &InferenceRequest,
6301        output_token_count: usize,
6302    ) -> Option<InferenceExecutionEvidence> {
6303        let requested = &request.evidence_request;
6304        if !requested.capture_prompt_token_ids && !requested.capture_engine_token_timing {
6305            return None;
6306        }
6307        Some(InferenceExecutionEvidence {
6308            prompt_token_ids: requested
6309                .capture_prompt_token_ids
6310                .then(|| vec![TokenId::new(101), TokenId::new(202), TokenId::new(303)])
6311                .unwrap_or_default(),
6312            output_token_ids: (0..output_token_count)
6313                .map(|index| TokenId::new(11 + index as u32))
6314                .collect(),
6315            engine_token_timing: requested.capture_engine_token_timing.then(|| {
6316                EngineTokenTimingEvidence {
6317                    clock_source: "rust_std_instant".to_string(),
6318                    wall_anchor_unix_nanos: 1_700_000_000_000_000_000,
6319                    wall_anchor_max_error_nanos: 500,
6320                    decode_ready_nanos_since_request_start: Some(1_000_000),
6321                    token_commit_nanos_since_request_start: (1..=output_token_count)
6322                        .map(|ordinal| ordinal as u64 * 1_000_000)
6323                        .collect(),
6324                    decode_stage_intervals: Vec::new(),
6325                }
6326            }),
6327        })
6328    }
6329
6330    #[async_trait]
6331    impl LlmInferenceEngine for StubLlm {
6332        async fn infer(
6333            &self,
6334            request: InferenceRequest,
6335        ) -> ferrum_types::Result<InferenceResponse> {
6336            let execution_evidence = stub_execution_evidence(&request, 2);
6337            Ok(InferenceResponse {
6338                request_id: request.id,
6339                text: self.text.clone(),
6340                tokens: vec![TokenId::new(11), TokenId::new(12)],
6341                finish_reason: self.finish_reason,
6342                usage: TokenUsage::new(7, 2),
6343                latency_ms: 1,
6344                created_at: chrono::Utc::now(),
6345                metadata: HashMap::new(),
6346                api_response: self.api_response.clone(),
6347                execution_evidence,
6348            })
6349        }
6350
6351        async fn infer_stream(
6352            &self,
6353            request: InferenceRequest,
6354        ) -> ferrum_types::Result<
6355            Pin<Box<dyn Stream<Item = ferrum_types::Result<StreamChunk>> + Send>>,
6356        > {
6357            if let Some(drop_notify) = self.pending_stream_drop_notify.as_ref() {
6358                return Ok(Box::pin(PendingDropStream {
6359                    drop_notify: Arc::clone(drop_notify),
6360                }));
6361            }
6362            if let Some(chunks) = &self.stream_chunks {
6363                let completion_token_count = self
6364                    .stream_usage
6365                    .as_ref()
6366                    .map(|usage| usage.completion_tokens)
6367                    .unwrap_or(chunks.len());
6368                let execution_evidence = stub_execution_evidence(&request, completion_token_count);
6369                let request_id = request.id;
6370                let mut stream_chunks = Vec::with_capacity(
6371                    chunks.len() + usize::from(self.stream_final_chunk_separate),
6372                );
6373                let last = chunks.len().saturating_sub(1);
6374                for (index, text) in chunks.iter().enumerate() {
6375                    let is_final_text_chunk = index == last && !self.stream_final_chunk_separate;
6376                    stream_chunks.push(Ok(StreamChunk {
6377                        request_id: request_id.clone(),
6378                        text: text.clone(),
6379                        token: (!(self.stream_tail_without_token && index == last))
6380                            .then_some(TokenId::new(11 + index as u32)),
6381                        finish_reason: is_final_text_chunk.then_some(self.finish_reason),
6382                        usage: is_final_text_chunk
6383                            .then(|| self.stream_usage.clone())
6384                            .flatten(),
6385                        created_at: chrono::Utc::now(),
6386                        metadata: HashMap::new(),
6387                        api_response: is_final_text_chunk
6388                            .then(|| self.api_response.clone())
6389                            .flatten(),
6390                        execution_evidence: is_final_text_chunk
6391                            .then(|| execution_evidence.clone())
6392                            .flatten(),
6393                    }));
6394                }
6395                if self.stream_final_chunk_separate {
6396                    stream_chunks.push(Ok(StreamChunk {
6397                        request_id,
6398                        text: String::new(),
6399                        token: None,
6400                        finish_reason: Some(self.finish_reason),
6401                        usage: self.stream_usage.clone(),
6402                        created_at: chrono::Utc::now(),
6403                        metadata: HashMap::new(),
6404                        api_response: self.api_response.clone(),
6405                        execution_evidence,
6406                    }));
6407                }
6408                return Ok(Box::pin(stream::iter(stream_chunks)));
6409            }
6410
6411            let execution_evidence = stub_execution_evidence(&request, 1);
6412            let chunk = StreamChunk {
6413                request_id: request.id,
6414                text: self.text.clone(),
6415                token: Some(TokenId::new(11)),
6416                finish_reason: Some(self.finish_reason),
6417                usage: self.stream_usage.clone(),
6418                created_at: chrono::Utc::now(),
6419                metadata: HashMap::new(),
6420                api_response: self.api_response.clone(),
6421                execution_evidence,
6422            };
6423            Ok(Box::pin(stream::iter(vec![Ok(chunk)])))
6424        }
6425    }
6426
6427    #[async_trait]
6428    impl LlmInferenceEngine for FailingLlm {
6429        async fn infer(
6430            &self,
6431            _request: InferenceRequest,
6432        ) -> ferrum_types::Result<InferenceResponse> {
6433            Err(self.infer_failure.clone())
6434        }
6435
6436        async fn infer_stream(
6437            &self,
6438            request: InferenceRequest,
6439        ) -> ferrum_types::Result<
6440            Pin<Box<dyn Stream<Item = ferrum_types::Result<StreamChunk>> + Send>>,
6441        > {
6442            if self.fail_after_stream_start {
6443                let _request_id = request.id;
6444                return Ok(Box::pin(stream::iter(vec![Err(self
6445                    .stream_chunk_failure
6446                    .clone())])));
6447            }
6448            Err(self.stream_start_failure.clone())
6449        }
6450    }
6451
6452    #[async_trait]
6453    impl LlmInferenceEngine for CapturingLlm {
6454        async fn infer(
6455            &self,
6456            request: InferenceRequest,
6457        ) -> ferrum_types::Result<InferenceResponse> {
6458            *self.last_request.lock().expect("capture lock") = Some(request.clone());
6459            Ok(InferenceResponse {
6460                request_id: request.id,
6461                text: "captured".to_string(),
6462                tokens: vec![TokenId::new(21)],
6463                finish_reason: FinishReason::Stop,
6464                usage: TokenUsage::new(9, 1),
6465                latency_ms: 1,
6466                created_at: chrono::Utc::now(),
6467                metadata: HashMap::new(),
6468                api_response: None,
6469                execution_evidence: None,
6470            })
6471        }
6472
6473        async fn infer_stream(
6474            &self,
6475            request: InferenceRequest,
6476        ) -> ferrum_types::Result<
6477            Pin<Box<dyn Stream<Item = ferrum_types::Result<StreamChunk>> + Send>>,
6478        > {
6479            *self.last_request.lock().expect("capture lock") = Some(request.clone());
6480            let chunk = StreamChunk {
6481                request_id: request.id,
6482                text: "captured".to_string(),
6483                token: Some(TokenId::new(21)),
6484                finish_reason: Some(FinishReason::Stop),
6485                usage: Some(TokenUsage::new(9, 1)),
6486                created_at: chrono::Utc::now(),
6487                metadata: HashMap::new(),
6488                api_response: None,
6489                execution_evidence: None,
6490            };
6491            Ok(Box::pin(stream::iter(vec![Ok(chunk)])))
6492        }
6493    }
6494
6495    fn state_with_stub(text: &str) -> AppState {
6496        AppState::default().with_llm(Arc::new(StubLlm::new(text)))
6497    }
6498
6499    fn router_with_stub(text: &str) -> Router {
6500        AxumServer::from_llm(Arc::new(StubLlm::new(text))).build_router()
6501    }
6502
6503    fn router_with_stub_and_template(text: &str, template: ModelChatTemplate) -> Router {
6504        AxumServer::from_llm(Arc::new(StubLlm::new(text)))
6505            .with_prompt_template(Some(template))
6506            .build_router()
6507    }
6508
6509    fn router_with_stub_and_request_dump_dir(text: &str, request_dump_dir: PathBuf) -> Router {
6510        AxumServer::from_state(
6511            AppState::default()
6512                .with_llm(Arc::new(StubLlm::new(text)))
6513                .with_request_dump_dir(Some(request_dump_dir)),
6514        )
6515        .build_router()
6516    }
6517
6518    fn router_with_stub_request_dump_and_profile(
6519        text: &str,
6520        request_dump_dir: PathBuf,
6521        profile_jsonl: PathBuf,
6522    ) -> Router {
6523        AxumServer::from_state(
6524            AppState::default()
6525                .with_llm(Arc::new(StubLlm::new(text)))
6526                .with_request_dump_dir(Some(request_dump_dir))
6527                .with_profile_detail(ferrum_types::ObservabilityProfileDetail::Latency)
6528                .with_profile_jsonl(Some(profile_jsonl)),
6529        )
6530        .build_router()
6531    }
6532
6533    fn router_with_stub_stream_chunks(chunks: &[&str]) -> Router {
6534        AxumServer::from_llm(Arc::new(StubLlm::with_stream_chunks(chunks))).build_router()
6535    }
6536
6537    fn router_with_stub_finish_reason(text: &str, finish_reason: FinishReason) -> Router {
6538        AxumServer::from_llm(Arc::new(StubLlm {
6539            finish_reason,
6540            ..StubLlm::new(text)
6541        }))
6542        .build_router()
6543    }
6544
6545    fn router_with_stub_stream_chunks_and_request_dump_dir(
6546        chunks: &[&str],
6547        request_dump_dir: PathBuf,
6548    ) -> Router {
6549        AxumServer::from_state(
6550            AppState::default()
6551                .with_llm(Arc::new(StubLlm::with_stream_chunks(chunks)))
6552                .with_request_dump_dir(Some(request_dump_dir)),
6553        )
6554        .build_router()
6555    }
6556
6557    fn router_with_stub_stream_request_dump_and_profile(
6558        chunks: &[&str],
6559        request_dump_dir: PathBuf,
6560        profile_jsonl: PathBuf,
6561    ) -> Router {
6562        AxumServer::from_state(
6563            AppState::default()
6564                .with_llm(Arc::new(StubLlm::with_stream_chunks(chunks)))
6565                .with_request_dump_dir(Some(request_dump_dir))
6566                .with_profile_detail(ferrum_types::ObservabilityProfileDetail::Latency)
6567                .with_profile_jsonl(Some(profile_jsonl)),
6568        )
6569        .build_router()
6570    }
6571
6572    fn router_with_stub_separate_final_stream_chunk(chunks: &[&str]) -> Router {
6573        AxumServer::from_llm(Arc::new(StubLlm::with_separate_final_stream_chunk(chunks)))
6574            .build_router()
6575    }
6576
6577    fn router_with_stub_api_response(
6578        text: &str,
6579        api_response: ferrum_types::ApiResponse,
6580    ) -> Router {
6581        AxumServer::from_llm(Arc::new(StubLlm::with_api_response(text, api_response)))
6582            .build_router()
6583    }
6584
6585    fn router_with_stub_api_response_and_finish_reason(
6586        text: &str,
6587        api_response: ferrum_types::ApiResponse,
6588        finish_reason: FinishReason,
6589    ) -> Router {
6590        AxumServer::from_llm(Arc::new(StubLlm::with_api_response_and_finish_reason(
6591            text,
6592            api_response,
6593            finish_reason,
6594        )))
6595        .build_router()
6596    }
6597
6598    fn weather_tool_api_response() -> ferrum_types::ApiResponse {
6599        ferrum_types::ApiResponse::Chat(ferrum_types::ApiChatResponse {
6600            message: ferrum_types::ApiChatMessage {
6601                role: ferrum_types::ApiMessageRole::Assistant,
6602                content: String::new(),
6603                name: None,
6604                tool_calls: vec![ferrum_types::ApiToolCall {
6605                    id: "call_1".to_string(),
6606                    tool_type: "function".to_string(),
6607                    function: ferrum_types::ApiFunctionCall {
6608                        name: "weather".to_string(),
6609                        arguments: "{\"city\":\"Paris\"}".to_string(),
6610                    },
6611                }],
6612                tool_call_id: None,
6613                function_call: None,
6614            },
6615            finish_reason: Some("tool_calls".to_string()),
6616        })
6617    }
6618
6619    fn weather_tool_api_response_with_commentary() -> ferrum_types::ApiResponse {
6620        let mut response = weather_tool_api_response();
6621        let ferrum_types::ApiResponse::Chat(chat) = &mut response else {
6622            unreachable!("weather response is chat")
6623        };
6624        chat.message.content = "I will check.".to_string();
6625        response
6626    }
6627
6628    fn namespaced_tool_api_response() -> ferrum_types::ApiResponse {
6629        ferrum_types::ApiResponse::Chat(ferrum_types::ApiChatResponse {
6630            message: ferrum_types::ApiChatMessage {
6631                role: ferrum_types::ApiMessageRole::Assistant,
6632                content: String::new(),
6633                name: None,
6634                tool_calls: vec![ferrum_types::ApiToolCall {
6635                    id: "call_ns_1".to_string(),
6636                    tool_type: "function".to_string(),
6637                    function: ferrum_types::ApiFunctionCall {
6638                        name: "collaboration__wait_agent".to_string(),
6639                        arguments: "{}".to_string(),
6640                    },
6641                }],
6642                tool_call_id: None,
6643                function_call: None,
6644            },
6645            finish_reason: Some("tool_calls".to_string()),
6646        })
6647    }
6648
6649    fn two_tool_api_response() -> ferrum_types::ApiResponse {
6650        ferrum_types::ApiResponse::Chat(ferrum_types::ApiChatResponse {
6651            message: ferrum_types::ApiChatMessage {
6652                role: ferrum_types::ApiMessageRole::Assistant,
6653                content: String::new(),
6654                name: None,
6655                tool_calls: vec![
6656                    ferrum_types::ApiToolCall {
6657                        id: "call_1".to_string(),
6658                        tool_type: "function".to_string(),
6659                        function: ferrum_types::ApiFunctionCall {
6660                            name: "weather".to_string(),
6661                            arguments: "{}".to_string(),
6662                        },
6663                    },
6664                    ferrum_types::ApiToolCall {
6665                        id: "call_2".to_string(),
6666                        tool_type: "function".to_string(),
6667                        function: ferrum_types::ApiFunctionCall {
6668                            name: "clock".to_string(),
6669                            arguments: "{}".to_string(),
6670                        },
6671                    },
6672                ],
6673                tool_call_id: None,
6674                function_call: None,
6675            },
6676            finish_reason: Some("tool_calls".to_string()),
6677        })
6678    }
6679
6680    fn router_with_stub_without_stream_usage(text: &str) -> Router {
6681        AxumServer::from_llm(Arc::new(StubLlm::without_stream_usage(text))).build_router()
6682    }
6683
6684    fn router_without_llm() -> Router {
6685        AxumServer::from_state(AppState::default()).build_router()
6686    }
6687
6688    fn router_with_failing_llm() -> Router {
6689        AxumServer::from_llm(Arc::new(FailingLlm::new())).build_router()
6690    }
6691
6692    fn router_with_failing_llm_and_request_dump_dir(request_dump_dir: PathBuf) -> Router {
6693        AxumServer::from_state(
6694            AppState::default()
6695                .with_llm(Arc::new(FailingLlm::new()))
6696                .with_request_dump_dir(Some(request_dump_dir)),
6697        )
6698        .build_router()
6699    }
6700
6701    fn router_with_failing_llm_request_dump_and_profile(
6702        request_dump_dir: PathBuf,
6703        profile_jsonl: PathBuf,
6704    ) -> Router {
6705        AxumServer::from_state(
6706            AppState::default()
6707                .with_llm(Arc::new(FailingLlm::new()))
6708                .with_request_dump_dir(Some(request_dump_dir))
6709                .with_profile_detail(ferrum_types::ObservabilityProfileDetail::Latency)
6710                .with_profile_jsonl(Some(profile_jsonl)),
6711        )
6712        .build_router()
6713    }
6714
6715    fn router_with_resource_exhausted_llm_and_request_dump_dir(
6716        request_dump_dir: PathBuf,
6717    ) -> Router {
6718        AxumServer::from_state(
6719            AppState::default()
6720                .with_llm(Arc::new(FailingLlm::resource_exhausted()))
6721                .with_request_dump_dir(Some(request_dump_dir)),
6722        )
6723        .build_router()
6724    }
6725
6726    fn router_with_stream_chunk_failing_llm() -> Router {
6727        AxumServer::from_llm(Arc::new(FailingLlm::after_stream_start())).build_router()
6728    }
6729
6730    fn router_with_stream_chunk_failing_llm_and_request_dump_dir(
6731        request_dump_dir: PathBuf,
6732    ) -> Router {
6733        AxumServer::from_state(
6734            AppState::default()
6735                .with_llm(Arc::new(FailingLlm::after_stream_start()))
6736                .with_request_dump_dir(Some(request_dump_dir)),
6737        )
6738        .build_router()
6739    }
6740
6741    fn router_with_capturing_llm() -> (Router, Arc<CapturingLlm>) {
6742        let engine = Arc::new(CapturingLlm::new());
6743        let registry = ServedModelRegistry::try_new(
6744            "qwen3",
6745            ServedModelKind::Llm,
6746            vec![
6747                "qwen3".to_string(),
6748                "stub-model".to_string(),
6749                "served-alias".to_string(),
6750            ],
6751            vec![],
6752        )
6753        .unwrap();
6754        let router = AxumServer::from_llm(engine.clone())
6755            .with_served_model_registry(registry)
6756            .build_router();
6757        (router, engine)
6758    }
6759
6760    fn unique_request_dump_dir(test_name: &str) -> PathBuf {
6761        let path =
6762            std::env::temp_dir().join(format!("ferrum-server-{test_name}-{}", Uuid::new_v4()));
6763        fs::create_dir_all(&path).expect("create request dump dir");
6764        path
6765    }
6766
6767    fn unique_profile_jsonl(test_name: &str) -> PathBuf {
6768        std::env::temp_dir().join(format!(
6769            "ferrum-server-{test_name}-{}.jsonl",
6770            Uuid::new_v4()
6771        ))
6772    }
6773
6774    fn only_replay_bundle(root: &Path) -> PathBuf {
6775        let mut dirs = fs::read_dir(root)
6776            .expect("read request dump dir")
6777            .filter_map(|entry| {
6778                let path = entry.expect("dir entry").path();
6779                path.is_dir().then_some(path)
6780            })
6781            .collect::<Vec<_>>();
6782        dirs.sort();
6783        assert_eq!(
6784            dirs.len(),
6785            1,
6786            "expected exactly one replay bundle in {root:?}"
6787        );
6788        dirs.remove(0)
6789    }
6790
6791    fn read_json_file(path: impl AsRef<Path>) -> Value {
6792        let path = path.as_ref();
6793        let text = fs::read_to_string(path).unwrap_or_else(|err| {
6794            panic!("failed to read {}: {}", path.display(), err);
6795        });
6796        serde_json::from_str(&text).unwrap_or_else(|err| {
6797            panic!("failed to parse {}: {}", path.display(), err);
6798        })
6799    }
6800
6801    fn read_profile_events(path: &Path) -> Vec<Value> {
6802        let text = fs::read_to_string(path)
6803            .unwrap_or_else(|err| panic!("failed to read {}: {}", path.display(), err));
6804        text.lines()
6805            .filter(|line| !line.trim().is_empty())
6806            .map(|line| serde_json::from_str::<Value>(line).expect("profile event json"))
6807            .collect()
6808    }
6809
6810    fn assert_chat_failure_replay_bundle(
6811        root: &Path,
6812        expected_phase: &str,
6813        expected_error_kind: &str,
6814        expected_message: &str,
6815    ) {
6816        let bundle = only_replay_bundle(root);
6817        let request = read_json_file(bundle.join("request.json"));
6818        let request_id = request["request_id"]
6819            .as_str()
6820            .expect("request id")
6821            .to_string();
6822        let bad_scan = read_json_file(bundle.join("bad_output_scan.json"));
6823        assert_eq!(bad_scan["request_id"], request_id);
6824        assert_eq!(bad_scan["failure_kind"], "error");
6825        assert_eq!(bad_scan["failure_phase"], expected_phase);
6826        assert_eq!(bad_scan["error_kind"], expected_error_kind);
6827
6828        let diagnostics = read_json_file(bundle.join("failure_diagnostics.json"));
6829        assert_eq!(diagnostics["request_id"], request_id);
6830        assert_eq!(diagnostics["failure_kind"], "error");
6831        assert_eq!(diagnostics["first_failure_event"]["phase"], expected_phase);
6832        assert_eq!(
6833            diagnostics["first_failure_event"]["error_kind"],
6834            expected_error_kind
6835        );
6836        assert_eq!(diagnostics["nearest_request_id"], request_id);
6837        assert!(diagnostics["log_excerpt"]
6838            .as_str()
6839            .expect("log excerpt")
6840            .contains(expected_message));
6841        assert!(bundle.join("replay.command.json").is_file());
6842    }
6843
6844    fn assert_chat_success_replay_bundle(
6845        root: &Path,
6846        expected_token_ids: &[u32],
6847        expected_finish_reason: &str,
6848        expected_output_text: &str,
6849    ) {
6850        let bundle = only_replay_bundle(root);
6851        let request = read_json_file(bundle.join("request.json"));
6852        let request_id = request["request_id"]
6853            .as_str()
6854            .expect("request id")
6855            .to_string();
6856        let prompt_tokens = read_json_file(bundle.join("prompt_token_ids.json"));
6857        assert_eq!(prompt_tokens["request_id"], request_id);
6858        assert_eq!(prompt_tokens["token_ids"], json!([101, 202, 303]));
6859        assert_eq!(prompt_tokens["token_count"], 3);
6860        assert!(prompt_tokens["unavailable_reason"].is_null());
6861        let output_tokens = read_json_file(bundle.join("output_token_ids.json"));
6862        assert_eq!(output_tokens["request_id"], request_id);
6863        assert_eq!(output_tokens["token_ids"], json!(expected_token_ids));
6864        assert_eq!(output_tokens["token_count"], expected_token_ids.len());
6865        assert_eq!(output_tokens["finish_reason"], expected_finish_reason);
6866        assert!(output_tokens["unavailable_reason"].is_null());
6867
6868        let bad_scan = read_json_file(bundle.join("bad_output_scan.json"));
6869        assert_eq!(bad_scan["request_id"], request_id);
6870        assert_eq!(bad_scan["bad_output"], false);
6871        assert_eq!(bad_scan["failure_kind"], serde_json::Value::Null);
6872        assert_eq!(
6873            bad_scan["output_chars"],
6874            expected_output_text.chars().count()
6875        );
6876        assert_eq!(
6877            bad_scan["classified_output_sha256"],
6878            sha256_hex(expected_output_text.as_bytes())
6879        );
6880
6881        let output_text_bytes = fs::read(bundle.join("output_text.txt")).unwrap();
6882        assert_eq!(bad_scan["output_sha256"], sha256_hex(&output_text_bytes));
6883        let output_text = String::from_utf8(output_text_bytes).unwrap();
6884        assert!(output_text.contains("[redacted actual output]"));
6885        assert!(output_text.contains(&format!(
6886            "sha256={}",
6887            sha256_hex(expected_output_text.as_bytes())
6888        )));
6889        assert!(output_text.contains(&format!("chars={}", expected_output_text.chars().count())));
6890
6891        let replay_body = read_json_file(bundle.join("replay_body.json"));
6892        assert_eq!(replay_body["messages"][0]["role"], "user");
6893        assert_eq!(replay_body["messages"][0]["content"], "[redacted]");
6894        assert_eq!(replay_body["messages"][0]["content_redacted"], true);
6895
6896        let replay = read_json_file(bundle.join("replay.command.json"));
6897        assert_eq!(replay["requires_running_server"], true);
6898        let argv = replay["argv"].as_array().expect("replay argv");
6899        assert!(argv.iter().any(|item| item == "--data-binary"));
6900        assert!(argv.iter().any(|item| {
6901            item.as_str()
6902                .is_some_and(|value| value.starts_with('@') && value.ends_with("replay_body.json"))
6903        }));
6904        assert_eq!(replay["engine_replay"]["requires_http_server"], false);
6905        let engine_argv = replay["engine_replay"]["argv"]
6906            .as_array()
6907            .expect("engine replay argv");
6908        assert!(engine_argv.iter().any(|item| item == "replay-bundle"));
6909    }
6910
6911    fn router_with_capturing_llm_and_template(
6912        template: ModelChatTemplate,
6913    ) -> (Router, Arc<CapturingLlm>) {
6914        router_with_capturing_llm_and_template_default(template, None)
6915    }
6916
6917    fn router_with_capturing_llm_and_template_default(
6918        template: ModelChatTemplate,
6919        default_enable_thinking: Option<bool>,
6920    ) -> (Router, Arc<CapturingLlm>) {
6921        let engine = Arc::new(CapturingLlm::new());
6922        let registry = ServedModelRegistry::try_new(
6923            "qwen3",
6924            ServedModelKind::Llm,
6925            vec!["served-alias".to_string()],
6926            vec![],
6927        )
6928        .unwrap();
6929        let router = AxumServer::from_llm(engine.clone())
6930            .with_served_model_registry(registry)
6931            .with_prompt_template(Some(template))
6932            .with_default_enable_thinking(default_enable_thinking)
6933            .build_router();
6934        (router, engine)
6935    }
6936
6937    fn qwen36_chat_template() -> ModelChatTemplate {
6938        ModelChatTemplate::new(
6939            include_str!("../tests/fixtures/chat_template/Qwen__Qwen3.6-35B-A3B/template.jinja"),
6940            "Qwen/Qwen3.6-35B-A3B",
6941        )
6942    }
6943
6944    async fn capture_qwen36_tool_history_request(
6945        reasoning_fields: Value,
6946        stream: bool,
6947    ) -> InferenceRequest {
6948        let mut assistant = json!({
6949            "role": "assistant",
6950            "content": null,
6951            "tool_calls": [{
6952                "id": "call_1",
6953                "type": "function",
6954                "function": {"name": "weather", "arguments": "{\"city\":\"Paris\"}"}
6955            }]
6956        });
6957        assistant
6958            .as_object_mut()
6959            .expect("assistant message object")
6960            .extend(
6961                reasoning_fields
6962                    .as_object()
6963                    .expect("reasoning fields object")
6964                    .clone(),
6965            );
6966        let (router, engine) = router_with_capturing_llm_and_template(qwen36_chat_template());
6967        let response = post_json(
6968            router,
6969            "/v1/chat/completions",
6970            json!({
6971                "model": "served-alias",
6972                "messages": [
6973                    {"role": "user", "content": "Use the weather tool."},
6974                    assistant,
6975                    {"role": "tool", "tool_call_id": "call_1", "content": "sunny"}
6976                ],
6977                "tools": [{
6978                    "type": "function",
6979                    "function": {
6980                        "name": "weather",
6981                        "description": "Get weather",
6982                        "parameters": {
6983                            "type": "object",
6984                            "properties": {"city": {"type": "string"}},
6985                            "required": ["city"]
6986                        }
6987                    }
6988                }],
6989                "stream": stream
6990            }),
6991        )
6992        .await;
6993        assert_eq!(response.status(), AxumStatusCode::OK);
6994        if stream {
6995            assert!(response_text(response).await.contains("[DONE]"));
6996        }
6997        engine.last_request()
6998    }
6999
7000    fn router_with_capturing_lora_llm() -> (Router, Arc<CapturingLlm>) {
7001        let engine = Arc::new(CapturingLlm::new());
7002        let router = AxumServer::from_llm(engine.clone())
7003            .with_lora_adapters(
7004                "qwen3",
7005                vec![LoraAdapterModel::new(
7006                    "sql",
7007                    "qwen3:sql",
7008                    "/tmp/sql-adapter",
7009                )],
7010            )
7011            .unwrap()
7012            .build_router();
7013        (router, engine)
7014    }
7015
7016    fn router_with_stub_embed() -> Router {
7017        AxumServer::from_embed(Arc::new(StubEmbed::new())).build_router()
7018    }
7019
7020    fn router_with_stub_transcribe() -> Router {
7021        AxumServer::from_transcribe(Arc::new(StubTranscribe::new())).build_router()
7022    }
7023
7024    fn router_with_stub_tts() -> Router {
7025        AxumServer::from_tts(Arc::new(StubTts::new())).build_router()
7026    }
7027
7028    async fn post_json(app: Router, path: &str, body: Value) -> Response {
7029        app.oneshot(
7030            Request::builder()
7031                .method("POST")
7032                .uri(path)
7033                .header(header::CONTENT_TYPE, "application/json")
7034                .body(Body::from(body.to_string()))
7035                .expect("request"),
7036        )
7037        .await
7038        .expect("route response")
7039    }
7040
7041    async fn post_json_with_benchmark_correlation(
7042        app: Router,
7043        path: &str,
7044        body: Value,
7045        correlation: &BenchmarkRequestCorrelation,
7046    ) -> Response {
7047        app.oneshot(
7048            Request::builder()
7049                .method("POST")
7050                .uri(path)
7051                .header(header::CONTENT_TYPE, "application/json")
7052                .header(BENCHMARK_RUN_ID_HEADER, &correlation.benchmark_run_id)
7053                .header(BENCHMARK_CELL_ID_HEADER, &correlation.cell_id)
7054                .header(
7055                    BENCHMARK_REPEAT_INDEX_HEADER,
7056                    correlation.repeat_index.to_string(),
7057                )
7058                .header(BENCHMARK_PHASE_HEADER, correlation.phase.as_str())
7059                .header(
7060                    BENCHMARK_REQUEST_INDEX_HEADER,
7061                    correlation.request_index.to_string(),
7062                )
7063                .body(Body::from(body.to_string()))
7064                .expect("request"),
7065        )
7066        .await
7067        .expect("route response")
7068    }
7069
7070    async fn post_raw_json(app: Router, path: &str, body: &str) -> Response {
7071        app.oneshot(
7072            Request::builder()
7073                .method("POST")
7074                .uri(path)
7075                .header(header::CONTENT_TYPE, "application/json")
7076                .body(Body::from(body.to_string()))
7077                .expect("request"),
7078        )
7079        .await
7080        .expect("route response")
7081    }
7082
7083    async fn post_multipart(app: Router, path: &str, boundary: &str, body: &str) -> Response {
7084        app.oneshot(
7085            Request::builder()
7086                .method("POST")
7087                .uri(path)
7088                .header(
7089                    header::CONTENT_TYPE,
7090                    format!("multipart/form-data; boundary={boundary}"),
7091                )
7092                .body(Body::from(body.to_string()))
7093                .expect("request"),
7094        )
7095        .await
7096        .expect("route response")
7097    }
7098
7099    async fn get(app: Router, path: &str) -> Response {
7100        app.oneshot(
7101            Request::builder()
7102                .method("GET")
7103                .uri(path)
7104                .body(Body::empty())
7105                .expect("request"),
7106        )
7107        .await
7108        .expect("route response")
7109    }
7110
7111    async fn response_json(response: Response) -> Value {
7112        let bytes = to_bytes(response.into_body(), usize::MAX)
7113            .await
7114            .expect("body bytes");
7115        serde_json::from_slice(&bytes).expect("json body")
7116    }
7117
7118    async fn response_text(response: Response) -> String {
7119        let bytes = to_bytes(response.into_body(), usize::MAX)
7120            .await
7121            .expect("body bytes");
7122        String::from_utf8(bytes.to_vec()).expect("utf8 body")
7123    }
7124
7125    fn responses_sse_json_events(body: &str) -> Vec<Value> {
7126        body.lines()
7127            .filter_map(|line| line.strip_prefix("data: "))
7128            .filter(|data| *data != "[DONE]")
7129            .map(|data| serde_json::from_str(data).expect("Responses SSE JSON event"))
7130            .collect()
7131    }
7132
7133    async fn response_bytes(response: Response) -> Vec<u8> {
7134        to_bytes(response.into_body(), usize::MAX)
7135            .await
7136            .expect("body bytes")
7137            .to_vec()
7138    }
7139
7140    async fn error_json(error: ServerError) -> (AxumStatusCode, Value) {
7141        let response = error.into_response();
7142        let status = response.status();
7143        (status, response_json(response).await)
7144    }
7145
7146    fn assert_openai_stream_error(body: &str, expected_message: &str) {
7147        assert!(body.contains("data: [DONE]"), "missing DONE: {body}");
7148        assert!(
7149            body.contains("\"error\":{\"message\":\""),
7150            "stream failure should emit OpenAI error envelope: {body}"
7151        );
7152        assert!(
7153            body.contains(expected_message),
7154            "stream failure should include engine error message {expected_message:?}: {body}"
7155        );
7156        assert!(
7157            body.contains("\"type\":\"internal_server_error\""),
7158            "stream failure should use internal_server_error: {body}"
7159        );
7160        assert!(
7161            !body.contains("{\"error\":\""),
7162            "stream failure must not use legacy bare error payload: {body}"
7163        );
7164    }
7165
7166    fn chat_request(extra: Value) -> ChatCompletionsRequest {
7167        let mut value = json!({
7168            "model": "stub-model",
7169            "messages": [{"role": "user", "content": "hello"}],
7170            "max_tokens": 8
7171        });
7172        let obj = value.as_object_mut().unwrap();
7173        for (k, v) in extra.as_object().unwrap() {
7174            obj.insert(k.clone(), v.clone());
7175        }
7176        serde_json::from_value(value).expect("chat request")
7177    }
7178
7179    #[tokio::test]
7180    async fn responses_route_returns_sync_text_and_usage() {
7181        let response = post_json(
7182            router_with_stub("hello from ferrum"),
7183            "/v1/responses",
7184            json!({
7185                "model": "stub-model",
7186                "input": "hello",
7187                "store": false
7188            }),
7189        )
7190        .await;
7191        assert_eq!(response.status(), AxumStatusCode::OK);
7192        let body = response_json(response).await;
7193        assert_eq!(body["object"], "response");
7194        assert_eq!(body["status"], "completed");
7195        assert_eq!(body["store"], false);
7196        assert_eq!(body["output"][0]["type"], "message");
7197        assert_eq!(body["output"][0]["phase"], "final_answer");
7198        assert_eq!(body["output"][0]["content"][0]["text"], "hello from ferrum");
7199        assert_eq!(body["usage"]["input_tokens"], 7);
7200        assert_eq!(body["usage"]["output_tokens"], 2);
7201        assert_eq!(body["usage"]["total_tokens"], 9);
7202        assert_eq!(body["presence_penalty"], 0.0);
7203        assert_eq!(body["frequency_penalty"], 0.0);
7204    }
7205
7206    #[tokio::test]
7207    async fn responses_route_streams_ordered_text_events_once() {
7208        let response = post_json(
7209            router_with_stub_stream_chunks(&["he", "llo"]),
7210            "/v1/responses",
7211            json!({
7212                "model": "stub-model",
7213                "input": [{"role": "user", "content": "say hello"}],
7214                "stream": true
7215            }),
7216        )
7217        .await;
7218        assert_eq!(response.status(), AxumStatusCode::OK);
7219        let body = response_text(response).await;
7220        for event in [
7221            "response.created",
7222            "response.output_item.added",
7223            "response.output_text.delta",
7224            "response.output_text.done",
7225            "response.output_item.done",
7226            "response.completed",
7227        ] {
7228            assert!(
7229                body.contains(&format!("event: {event}")),
7230                "missing {event}: {body}"
7231            );
7232        }
7233        assert_eq!(
7234            body.matches("event: response.completed").count(),
7235            1,
7236            "completed must be emitted exactly once: {body}"
7237        );
7238        assert!(
7239            body.contains("\"delta\":\"he\""),
7240            "missing first delta: {body}"
7241        );
7242        assert!(
7243            body.contains("\"delta\":\"llo\""),
7244            "missing second delta: {body}"
7245        );
7246        assert!(body.contains("\"input_tokens\":5"), "missing usage: {body}");
7247        let events = responses_sse_json_events(&body);
7248        let message_added = events
7249            .iter()
7250            .find(|event| {
7251                event["type"] == "response.output_item.added" && event["item"]["type"] == "message"
7252            })
7253            .expect("message item added");
7254        assert!(
7255            message_added["item"].get("phase").is_none(),
7256            "stream must not guess phase before later tool calls are known: {body}"
7257        );
7258        let message_done = events
7259            .iter()
7260            .find(|event| {
7261                event["type"] == "response.output_item.done" && event["item"]["type"] == "message"
7262            })
7263            .expect("message item done");
7264        assert_eq!(message_done["item"]["phase"], "final_answer");
7265        let terminal = events
7266            .iter()
7267            .find(|event| event["type"] == "response.completed")
7268            .expect("completed response");
7269        assert_eq!(terminal["response"]["output"][0]["phase"], "final_answer");
7270        let completed = body
7271            .find("event: response.completed")
7272            .expect("completed event");
7273        let done = body.find("data: [DONE]").expect("terminal DONE marker");
7274        assert!(
7275            completed < done,
7276            "DONE must follow response.completed: {body}"
7277        );
7278    }
7279
7280    #[tokio::test]
7281    async fn responses_route_supports_stateless_function_round_trip() {
7282        let tool = json!({
7283            "type": "function",
7284            "name": "weather",
7285            "description": "Get weather",
7286            "parameters": {
7287                "type": "object",
7288                "properties": {"city": {"type": "string"}},
7289                "required": ["city"]
7290            }
7291        });
7292        let first = post_json(
7293            router_with_stub_api_response("", weather_tool_api_response()),
7294            "/v1/responses",
7295            json!({
7296                "model": "stub-model",
7297                "input": "Use the weather tool",
7298                "tools": [tool.clone()],
7299                "tool_choice": "auto"
7300            }),
7301        )
7302        .await;
7303        assert_eq!(first.status(), AxumStatusCode::OK);
7304        let first_body = response_json(first).await;
7305        let call = first_body["output"][0].clone();
7306        assert_eq!(call["type"], "function_call");
7307        assert_eq!(call["call_id"], "call_1");
7308        assert_eq!(call["name"], "weather");
7309        assert_eq!(call["arguments"], "{\"city\":\"Paris\"}");
7310
7311        let second = post_json(
7312            router_with_stub("weather received"),
7313            "/v1/responses",
7314            json!({
7315                "model": "stub-model",
7316                "input": [
7317                    {"role": "user", "content": "Use the weather tool"},
7318                    call,
7319                    {"type": "function_call_output", "call_id": "call_1", "output": "sunny"}
7320                ],
7321                "tools": [tool]
7322            }),
7323        )
7324        .await;
7325        assert_eq!(second.status(), AxumStatusCode::OK);
7326        let second_body = response_json(second).await;
7327        assert_eq!(
7328            second_body["output"][0]["content"][0]["text"],
7329            "weather received"
7330        );
7331    }
7332
7333    #[tokio::test]
7334    async fn responses_route_marks_text_before_calls_as_commentary() {
7335        let request = || {
7336            json!({
7337                "model": "stub-model",
7338                "input": "Use the weather tool",
7339                "stream": false,
7340                "tools": [{
7341                    "type": "function",
7342                    "name": "weather",
7343                    "parameters": {"type": "object"}
7344                }]
7345            })
7346        };
7347        let sync = post_json(
7348            router_with_stub_api_response("", weather_tool_api_response_with_commentary()),
7349            "/v1/responses",
7350            request(),
7351        )
7352        .await;
7353        assert_eq!(sync.status(), AxumStatusCode::OK);
7354        let sync = response_json(sync).await;
7355        assert_eq!(sync["output"][0]["type"], "message");
7356        assert_eq!(sync["output"][0]["phase"], "commentary");
7357        assert_eq!(sync["output"][1]["type"], "function_call");
7358
7359        let mut stream_request = request();
7360        stream_request["stream"] = json!(true);
7361        let stream = post_json(
7362            router_with_stub_api_response("", weather_tool_api_response_with_commentary()),
7363            "/v1/responses",
7364            stream_request,
7365        )
7366        .await;
7367        assert_eq!(stream.status(), AxumStatusCode::OK);
7368        let body = response_text(stream).await;
7369        let events = responses_sse_json_events(&body);
7370        let message_added = events
7371            .iter()
7372            .find(|event| {
7373                event["type"] == "response.output_item.added" && event["item"]["type"] == "message"
7374            })
7375            .expect("message item added");
7376        assert!(message_added["item"].get("phase").is_none());
7377        let message_done = events
7378            .iter()
7379            .find(|event| {
7380                event["type"] == "response.output_item.done" && event["item"]["type"] == "message"
7381            })
7382            .expect("message item done");
7383        assert_eq!(message_done["item"]["phase"], "commentary");
7384        let terminal = events
7385            .iter()
7386            .find(|event| event["type"] == "response.completed")
7387            .expect("completed response");
7388        assert_eq!(terminal["response"]["output"][0]["phase"], "commentary");
7389        assert_eq!(terminal["response"]["output"][1]["type"], "function_call");
7390    }
7391
7392    #[tokio::test]
7393    async fn responses_route_accepts_real_caller_owned_second_turn_shape() {
7394        let response = post_json(
7395            router_with_stub("You first said hello."),
7396            "/v1/responses",
7397            json!({
7398                "model": "stub-model",
7399                "instructions": "Answer from the supplied history.",
7400                "input": [
7401                    {
7402                        "type": "message",
7403                        "role": "user",
7404                        "content": [{"type": "input_text", "text": "Hello"}]
7405                    },
7406                    {
7407                        "type": "message",
7408                        "role": "assistant",
7409                        "content": [{"type": "output_text", "text": "Hi there!"}]
7410                    },
7411                    {
7412                        "type": "reasoning",
7413                        "encrypted_content": null,
7414                        "summary": []
7415                    },
7416                    {
7417                        "type": "message",
7418                        "role": "user",
7419                        "content": [{"type": "input_text", "text": "What did I say first?"}]
7420                    }
7421                ],
7422                "store": false,
7423                "stream": false,
7424                "include": ["reasoning.encrypted_content"],
7425                "parallel_tool_calls": false,
7426                "prompt_cache_key": "thread-1",
7427                "reasoning": {"effort": "high", "summary": "auto"}
7428            }),
7429        )
7430        .await;
7431        assert_eq!(response.status(), AxumStatusCode::OK);
7432        let body = response_json(response).await;
7433        assert_eq!(
7434            body["output"][0]["content"][0]["text"],
7435            "You first said hello."
7436        );
7437        assert_eq!(body["parallel_tool_calls"], false);
7438        assert_eq!(body["prompt_cache_key"], "thread-1");
7439        assert_eq!(body["reasoning"]["effort"], "high");
7440    }
7441
7442    #[tokio::test]
7443    async fn responses_route_merges_instructions_with_leading_developer_message() {
7444        let template = ModelChatTemplate::new(
7445            "{% for message in messages %}{% if message.role == 'system' and not loop.first %}{{ raise_exception('System message must be at the beginning.') }}{% endif %}[{{ message.role }}]{{ message.content }}{% endfor %}",
7446            "strict-leading-system-template",
7447        );
7448        let response = post_json(
7449            router_with_stub_and_template("ok", template),
7450            "/v1/responses",
7451            json!({
7452                "model": "stub-model",
7453                "instructions": "Top-level instructions",
7454                "input": [
7455                    {"type": "message", "role": "developer", "content": "Developer instructions"},
7456                    {"type": "message", "role": "user", "content": "Hello"}
7457                ]
7458            }),
7459        )
7460        .await;
7461        assert_eq!(response.status(), AxumStatusCode::OK);
7462    }
7463
7464    #[tokio::test]
7465    async fn responses_route_adapts_interleaved_system_for_strict_template() {
7466        let template = ModelChatTemplate::new(
7467            "{% for message in messages %}{% if message.role == 'system' and not loop.first %}{{ raise_exception('System message must be at the beginning.') }}{% endif %}{% endfor %}{% if messages|length != 2 %}{{ raise_exception('expected two messages') }}{% endif %}{% if messages[0].role != 'system' %}{{ raise_exception('system must be first') }}{% endif %}{% if messages[0].content != 'Initial instructions\\n\\nDeferred tool instructions' %}{{ raise_exception('system instructions were not preserved') }}{% endif %}[{{ messages[0].role }}]{{ messages[0].content }}[{{ messages[1].role }}]{{ messages[1].content }}",
7468            "strict-leading-system-template",
7469        );
7470        let response = post_json(
7471            router_with_stub_and_template("ok", template),
7472            "/v1/responses",
7473            json!({
7474                "model": "stub-model",
7475                "input": [
7476                    {"role": "system", "content": "Initial instructions"},
7477                    {"role": "user", "content": "Use the available tool"},
7478                    {"role": "developer", "content": "Deferred tool instructions"}
7479                ]
7480            }),
7481        )
7482        .await;
7483        assert_eq!(response.status(), AxumStatusCode::OK);
7484    }
7485
7486    #[tokio::test]
7487    async fn responses_route_keeps_phase_aligned_through_system_injection() {
7488        let template = ModelChatTemplate::new(
7489            "{% if messages|length != 3 %}{{ raise_exception('expected three messages') }}{% endif %}{% if messages[0].role != 'system' %}{{ raise_exception('system must be first') }}{% endif %}{% if messages[1].role != 'assistant' or messages[1].phase != 'commentary' %}{{ raise_exception('assistant phase was not preserved') }}{% endif %}{% if messages[2].role != 'user' or messages[2].phase is defined %}{{ raise_exception('phase metadata shifted') }}{% endif %}[assistant]",
7490            "phase-alignment-template",
7491        );
7492        let response = post_json(
7493            router_with_stub_and_template(r#"{"ok":true}"#, template),
7494            "/v1/responses",
7495            json!({
7496                "model": "stub-model",
7497                "instructions": "Top-level instructions",
7498                "input": [
7499                    {
7500                        "type": "message",
7501                        "role": "assistant",
7502                        "phase": "commentary",
7503                        "content": "I will inspect."
7504                    },
7505                    {"type": "message", "role": "user", "content": "Continue"}
7506                ],
7507                "text": {"format": {"type": "json_object"}}
7508            }),
7509        )
7510        .await;
7511        assert_eq!(response.status(), AxumStatusCode::OK);
7512    }
7513
7514    #[tokio::test]
7515    async fn responses_route_can_disable_interleaved_system_coalescing() {
7516        let template = ModelChatTemplate::new(
7517            "{% for message in messages %}{% if message.role == 'system' and not loop.first %}{{ raise_exception('System message must be at the beginning.') }}{% endif %}[{{ message.role }}]{{ message.content }}{% endfor %}",
7518            "strict-leading-system-template",
7519        );
7520        let router = AxumServer::from_state(
7521            AppState::default()
7522                .with_llm(Arc::new(StubLlm::new("ok")))
7523                .with_prompt_template(Some(template))
7524                .with_interleaved_system_coalescing(false),
7525        )
7526        .build_router();
7527        let response = post_json(
7528            router,
7529            "/v1/responses",
7530            json!({
7531                "model": "stub-model",
7532                "input": [
7533                    {"role": "system", "content": "Initial instructions"},
7534                    {"role": "user", "content": "Use the available tool"},
7535                    {"role": "developer", "content": "Deferred tool instructions"}
7536                ]
7537            }),
7538        )
7539        .await;
7540        assert_eq!(response.status(), AxumStatusCode::INTERNAL_SERVER_ERROR);
7541        let body = response_json(response).await;
7542        assert!(
7543            body.to_string()
7544                .contains("System message must be at the beginning."),
7545            "{body}"
7546        );
7547    }
7548
7549    #[tokio::test]
7550    async fn chat_route_applies_and_can_disable_interleaved_system_coalescing() {
7551        let template = || {
7552            ModelChatTemplate::new(
7553                "{% for message in messages %}{% if message.role == 'system' and not loop.first %}{{ raise_exception('System message must be at the beginning.') }}{% endif %}[{{ message.role }}]{{ message.content }}{% endfor %}",
7554                "strict-leading-system-template",
7555            )
7556        };
7557        let request = || {
7558            json!({
7559                "model": "stub-model",
7560                "messages": [
7561                    {"role": "system", "content": "Initial instructions"},
7562                    {"role": "user", "content": "Use the available tool"},
7563                    {"role": "system", "content": "Deferred tool instructions"}
7564                ]
7565            })
7566        };
7567
7568        let enabled = post_json(
7569            router_with_stub_and_template("ok", template()),
7570            "/v1/chat/completions",
7571            request(),
7572        )
7573        .await;
7574        assert_eq!(enabled.status(), AxumStatusCode::OK);
7575
7576        let consecutive = post_json(
7577            router_with_stub_and_template("ok", template()),
7578            "/v1/chat/completions",
7579            json!({
7580                "model": "stub-model",
7581                "messages": [
7582                    {"role": "system", "content": "Initial instructions"},
7583                    {"role": "system", "content": "Deferred tool instructions"},
7584                    {"role": "user", "content": "Use the available tool"}
7585                ]
7586            }),
7587        )
7588        .await;
7589        assert_eq!(consecutive.status(), AxumStatusCode::OK);
7590
7591        let disabled_router = AxumServer::from_state(
7592            AppState::default()
7593                .with_llm(Arc::new(StubLlm::new("ok")))
7594                .with_prompt_template(Some(template()))
7595                .with_interleaved_system_coalescing(false),
7596        )
7597        .build_router();
7598        let disabled = post_json(disabled_router, "/v1/chat/completions", request()).await;
7599        assert_eq!(disabled.status(), AxumStatusCode::INTERNAL_SERVER_ERROR);
7600        let body = response_json(disabled).await;
7601        assert!(
7602            body.to_string()
7603                .contains("System message must be at the beginning."),
7604            "{body}"
7605        );
7606    }
7607
7608    #[tokio::test]
7609    async fn responses_route_keeps_structured_output_to_one_leading_system_message() {
7610        let template = ModelChatTemplate::new(
7611            "{% for message in messages %}{% if message.role == 'system' and not loop.first %}{{ raise_exception('System message must be at the beginning.') }}{% endif %}[{{ message.role }}]{{ message.content }}{% endfor %}",
7612            "strict-leading-system-template",
7613        );
7614        let response = post_json(
7615            router_with_stub_and_template(r#"{"ok":true}"#, template),
7616            "/v1/responses",
7617            json!({
7618                "model": "stub-model",
7619                "instructions": "Top-level instructions",
7620                "input": [
7621                    {"type": "message", "role": "developer", "content": "Developer instructions"},
7622                    {"type": "message", "role": "user", "content": "Return JSON"}
7623                ],
7624                "text": {"format": {"type": "json_object"}}
7625            }),
7626        )
7627        .await;
7628        assert_eq!(response.status(), AxumStatusCode::OK);
7629        let body = response_json(response).await;
7630        assert_eq!(body["output"][0]["content"][0]["text"], r#"{"ok":true}"#);
7631    }
7632
7633    #[tokio::test]
7634    async fn responses_route_output_can_be_replayed_with_readable_reasoning() {
7635        let first = post_json(
7636            router_with_stub("<think>Checked the supplied facts.</think>\nFirst answer"),
7637            "/v1/responses",
7638            json!({
7639                "model": "stub-model",
7640                "input": "First question",
7641                "include": ["reasoning.encrypted_content"]
7642            }),
7643        )
7644        .await;
7645        assert_eq!(first.status(), AxumStatusCode::OK);
7646        let first_body = response_json(first).await;
7647        assert_eq!(first_body["output"][0]["type"], "reasoning");
7648        assert_eq!(
7649            first_body["output"][0]["content"][0]["text"],
7650            "Checked the supplied facts."
7651        );
7652        assert_eq!(first_body["output"][0]["encrypted_content"], Value::Null);
7653        assert_eq!(first_body["output"][1]["type"], "message");
7654
7655        let mut input = vec![json!({
7656            "type": "message",
7657            "role": "user",
7658            "content": [{"type": "input_text", "text": "First question"}]
7659        })];
7660        input.extend(first_body["output"].as_array().unwrap().iter().cloned());
7661        input.push(json!({
7662            "type": "message",
7663            "role": "user",
7664            "content": [{"type": "input_text", "text": "Continue"}]
7665        }));
7666        let second = post_json(
7667            router_with_stub("Second answer"),
7668            "/v1/responses",
7669            json!({"model": "stub-model", "input": input}),
7670        )
7671        .await;
7672        assert_eq!(second.status(), AxumStatusCode::OK);
7673        let second_body = response_json(second).await;
7674        assert_eq!(
7675            second_body["output"][0]["content"][0]["text"],
7676            "Second answer"
7677        );
7678    }
7679
7680    #[tokio::test]
7681    async fn responses_route_streams_reasoning_before_text_with_stable_indices() {
7682        let response = post_json(
7683            router_with_stub_stream_chunks(&["<think>inspect", " history</think>\nfinal"]),
7684            "/v1/responses",
7685            json!({
7686                "model": "stub-model",
7687                "input": "answer",
7688                "stream": true,
7689                "include": ["reasoning.encrypted_content"]
7690            }),
7691        )
7692        .await;
7693        assert_eq!(response.status(), AxumStatusCode::OK);
7694        let body = response_text(response).await;
7695        let events = responses_sse_json_events(&body);
7696        for (sequence, event) in events.iter().enumerate() {
7697            assert_eq!(
7698                event["sequence_number"], sequence,
7699                "Responses sequence numbers must be contiguous: {body}"
7700            );
7701        }
7702        for event in [
7703            "response.reasoning_text.delta",
7704            "response.reasoning_text.done",
7705            "response.output_text.delta",
7706            "response.completed",
7707        ] {
7708            assert!(
7709                body.contains(&format!("event: {event}")),
7710                "missing {event}: {body}"
7711            );
7712        }
7713        let reasoning_done = body
7714            .find("event: response.reasoning_text.done")
7715            .expect("reasoning done");
7716        let text_added = body[reasoning_done..]
7717            .find("event: response.output_item.added")
7718            .map(|offset| reasoning_done + offset)
7719            .expect("text item added");
7720        assert!(
7721            reasoning_done < text_added,
7722            "reasoning must finish before text: {body}"
7723        );
7724        assert!(
7725            body.contains("\"output_index\":0,\"content_index\":0,\"delta\":\"inspect"),
7726            "reasoning must use output index 0: {body}"
7727        );
7728        assert!(
7729            body.contains("\"output_index\":1,\"content_index\":0,\"delta\":\"final"),
7730            "text must use output index 1: {body}"
7731        );
7732        let reasoning_added = events
7733            .iter()
7734            .find(|event| {
7735                event["type"] == "response.output_item.added"
7736                    && event["item"]["type"] == "reasoning"
7737            })
7738            .expect("reasoning item added");
7739        assert_eq!(reasoning_added["item"]["status"], "in_progress");
7740        let reasoning_part_added = events
7741            .iter()
7742            .find(|event| {
7743                event["type"] == "response.content_part.added"
7744                    && event["part"]["type"] == "reasoning_text"
7745            })
7746            .expect("reasoning content part added");
7747        assert_eq!(reasoning_part_added["output_index"], 0);
7748        let reasoning_item_done = events
7749            .iter()
7750            .find(|event| {
7751                event["type"] == "response.output_item.done" && event["item"]["type"] == "reasoning"
7752            })
7753            .expect("reasoning item done");
7754        assert_eq!(reasoning_item_done["item"]["status"], "completed");
7755        let terminal = events
7756            .iter()
7757            .find(|event| event["type"] == "response.completed")
7758            .expect("terminal response");
7759        assert_eq!(
7760            terminal["response"]["output"][0],
7761            reasoning_item_done["item"]
7762        );
7763        assert!(
7764            body.contains("data: [DONE]"),
7765            "missing terminal marker: {body}"
7766        );
7767    }
7768
7769    #[tokio::test]
7770    async fn responses_route_streams_function_call_events() {
7771        let response = post_json(
7772            router_with_stub_api_response("", weather_tool_api_response()),
7773            "/v1/responses",
7774            json!({
7775                "model": "stub-model",
7776                "input": "Use the weather tool",
7777                "stream": true,
7778                "tools": [{
7779                    "type": "function",
7780                    "name": "weather",
7781                    "parameters": {"type": "object"}
7782                }]
7783            }),
7784        )
7785        .await;
7786        assert_eq!(response.status(), AxumStatusCode::OK);
7787        let body = response_text(response).await;
7788        assert!(
7789            body.contains("event: response.function_call_arguments.delta"),
7790            "missing function delta: {body}"
7791        );
7792        assert!(
7793            body.contains("event: response.function_call_arguments.done"),
7794            "missing function done: {body}"
7795        );
7796        assert!(
7797            body.contains("\"call_id\":\"call_1\""),
7798            "missing call id: {body}"
7799        );
7800        assert_eq!(body.matches("event: response.completed").count(), 1);
7801    }
7802
7803    #[tokio::test]
7804    async fn responses_route_round_trips_namespace_identity_without_leaking_chat_alias() {
7805        let namespace_tool = json!({
7806            "type": "namespace",
7807            "name": "collaboration",
7808            "description": "Agent coordination tools",
7809            "tools": [{
7810                "type": "function",
7811                "name": "wait_agent",
7812                "parameters": {"type": "object"}
7813            }]
7814        });
7815        let sync = post_json(
7816            router_with_stub_api_response("", namespaced_tool_api_response()),
7817            "/v1/responses",
7818            json!({
7819                "model": "stub-model",
7820                "input": "Wait for the agent",
7821                "tools": [namespace_tool.clone()]
7822            }),
7823        )
7824        .await;
7825        assert_eq!(sync.status(), AxumStatusCode::OK);
7826        let sync_body = response_json(sync).await;
7827        assert_eq!(sync_body["output"][0]["type"], "function_call");
7828        assert_eq!(sync_body["output"][0]["namespace"], "collaboration");
7829        assert_eq!(sync_body["output"][0]["name"], "wait_agent");
7830
7831        let stream = post_json(
7832            router_with_stub_api_response("", namespaced_tool_api_response()),
7833            "/v1/responses",
7834            json!({
7835                "model": "stub-model",
7836                "input": "Wait for the agent",
7837                "tools": [namespace_tool],
7838                "stream": true
7839            }),
7840        )
7841        .await;
7842        assert_eq!(stream.status(), AxumStatusCode::OK);
7843        let stream_body = response_text(stream).await;
7844        let events = responses_sse_json_events(&stream_body);
7845        let function_events = events
7846            .iter()
7847            .filter(|event| {
7848                event["item"]["type"] == "function_call"
7849                    || event["type"] == "response.function_call_arguments.done"
7850            })
7851            .collect::<Vec<_>>();
7852        assert!(!function_events.is_empty());
7853        for event in function_events {
7854            let value = event.get("item").unwrap_or(event);
7855            assert_eq!(value["namespace"], "collaboration");
7856            assert_eq!(value["name"], "wait_agent");
7857        }
7858        assert!(stream_body.contains("data: [DONE]"));
7859        assert!(!stream_body.contains("collaboration__wait_agent"));
7860    }
7861
7862    #[tokio::test]
7863    async fn responses_route_enforces_parallel_tool_call_constraint() {
7864        let tools = json!([
7865            {"type": "function", "name": "weather", "parameters": {"type": "object"}},
7866            {"type": "function", "name": "clock", "parameters": {"type": "object"}}
7867        ]);
7868        let sync = post_json(
7869            router_with_stub_api_response("", two_tool_api_response()),
7870            "/v1/responses",
7871            json!({
7872                "model": "stub-model",
7873                "input": "Use both tools",
7874                "tools": tools.clone(),
7875                "parallel_tool_calls": false
7876            }),
7877        )
7878        .await;
7879        assert_eq!(sync.status(), AxumStatusCode::INTERNAL_SERVER_ERROR);
7880
7881        let stream = post_json(
7882            router_with_stub_api_response("", two_tool_api_response()),
7883            "/v1/responses",
7884            json!({
7885                "model": "stub-model",
7886                "input": "Use both tools",
7887                "tools": tools,
7888                "parallel_tool_calls": false,
7889                "stream": true
7890            }),
7891        )
7892        .await;
7893        assert_eq!(stream.status(), AxumStatusCode::OK);
7894        let body = response_text(stream).await;
7895        assert!(
7896            body.contains("event: response.failed"),
7897            "missing failure: {body}"
7898        );
7899        assert!(
7900            !body.contains("event: response.completed"),
7901            "must not complete: {body}"
7902        );
7903        assert!(
7904            body.contains("data: [DONE]"),
7905            "missing terminal marker: {body}"
7906        );
7907    }
7908
7909    #[tokio::test]
7910    async fn responses_route_streams_incomplete_terminal_event() {
7911        let response = post_json(
7912            router_with_stub_finish_reason("partial", FinishReason::Length),
7913            "/v1/responses",
7914            json!({"model": "stub-model", "input": "answer", "stream": true}),
7915        )
7916        .await;
7917        assert_eq!(response.status(), AxumStatusCode::OK);
7918        let body = response_text(response).await;
7919        assert!(
7920            body.contains("event: response.incomplete"),
7921            "missing incomplete terminal event: {body}"
7922        );
7923        assert!(
7924            !body.contains("event: response.completed"),
7925            "incomplete response must not emit completed: {body}"
7926        );
7927        assert!(
7928            body.contains("data: [DONE]"),
7929            "missing terminal marker: {body}"
7930        );
7931        let events = responses_sse_json_events(&body);
7932        let output_done = events
7933            .iter()
7934            .find(|event| event["type"] == "response.output_item.done")
7935            .expect("incomplete output item done event");
7936        assert_eq!(output_done["item"]["status"], "incomplete");
7937        let terminal = events
7938            .iter()
7939            .find(|event| event["type"] == "response.incomplete")
7940            .expect("incomplete terminal event");
7941        assert_eq!(terminal["response"]["output"][0]["status"], "incomplete");
7942    }
7943
7944    #[tokio::test]
7945    async fn responses_route_marks_sync_length_output_incomplete() {
7946        let response = post_json(
7947            router_with_stub_finish_reason("partial", FinishReason::Length),
7948            "/v1/responses",
7949            json!({"model": "stub-model", "input": "answer"}),
7950        )
7951        .await;
7952        assert_eq!(response.status(), AxumStatusCode::OK);
7953        let body = response_json(response).await;
7954        assert_eq!(body["status"], "incomplete");
7955        assert_eq!(body["output"][0]["status"], "incomplete");
7956    }
7957
7958    #[tokio::test]
7959    async fn responses_route_rejects_state_and_non_function_tools() {
7960        for (extra, param) in [
7961            (json!({"store": true}), "store"),
7962            (
7963                json!({"previous_response_id": "resp_previous"}),
7964                "previous_response_id",
7965            ),
7966            (
7967                json!({"tools": [{"type": "mcp", "server_label": "docs"}]}),
7968                "tools[0].type",
7969            ),
7970        ] {
7971            let mut body = json!({"model": "stub-model", "input": "hello"});
7972            body.as_object_mut()
7973                .unwrap()
7974                .extend(extra.as_object().unwrap().clone());
7975            let response = post_json(router_with_stub("unused"), "/v1/responses", body).await;
7976            assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
7977            let error = response_json(response).await;
7978            assert_eq!(error["error"]["param"], param, "error: {error}");
7979        }
7980    }
7981
7982    #[tokio::test]
7983    async fn responses_mvp_keeps_chat_completions_route_working() {
7984        let response = post_json(
7985            router_with_stub("chat still works"),
7986            "/v1/chat/completions",
7987            json!({
7988                "model": "stub-model",
7989                "messages": [{"role": "user", "content": "hello"}]
7990            }),
7991        )
7992        .await;
7993        assert_eq!(response.status(), AxumStatusCode::OK);
7994        let body = response_json(response).await;
7995        assert_eq!(body["choices"][0]["message"]["content"], "chat still works");
7996    }
7997
7998    #[test]
7999    fn sanitized_chat_request_body_redacts_user_text_and_secret_metadata() {
8000        let request = chat_request(json!({
8001            "messages": [{"role": "user", "content": "private prompt"}],
8002            "metadata": {"api_key": "should-not-survive"},
8003            "stream": true
8004        }));
8005        let body = sanitized_chat_request_body(&request);
8006        assert_eq!(body["model"], "stub-model");
8007        assert_eq!(body["stream"], true);
8008        assert_eq!(body["messages"][0]["role"], "user");
8009        assert_eq!(body["messages"][0]["content"], "[redacted]");
8010        assert_eq!(body["messages"][0]["content_redacted"], true);
8011        assert_eq!(body["messages"][0]["content_chars"], 14);
8012        assert_eq!(body["metadata"]["api_key"], "[redacted]");
8013    }
8014
8015    #[test]
8016    fn admission_health_prefers_runtime_authority_over_preflight_estimate() {
8017        let engine_status = EngineStatus {
8018            is_ready: true,
8019            loaded_models: Vec::new(),
8020            active_requests: 2,
8021            queued_requests: 1,
8022            memory_usage: MemoryUsage {
8023                total_bytes: 0,
8024                used_bytes: 0,
8025                free_bytes: 0,
8026                gpu_memory_bytes: None,
8027                cpu_memory_bytes: None,
8028                cache_memory_bytes: 0,
8029                utilization_percent: 0.0,
8030            },
8031            uptime_seconds: 0,
8032            last_heartbeat: chrono::Utc::now(),
8033            version: "test".to_owned(),
8034        };
8035        let runtime = ferrum_types::ExecutorAdmissionSnapshot::new(
8036            ferrum_types::ExecutionResourceAuthority::PlanRuntime,
8037            ferrum_types::ExecutorAdmissionLimits::new(32, 4096).unwrap(),
8038            2,
8039            7,
8040            23,
8041            None,
8042            Some(3),
8043        )
8044        .unwrap();
8045        let admission = admission_health_json(
8046            &engine_status,
8047            &EngineMetrics::default(),
8048            &json!({
8049                "admission": {
8050                    "effective_max_concurrent": 16,
8051                    "scheduler_policy": "continuous"
8052                }
8053            }),
8054            Some(&runtime),
8055            None,
8056        );
8057
8058        assert_eq!(admission["source"], "runtime_executor");
8059        assert_eq!(admission["runtime_snapshot_available"], true);
8060        assert_eq!(admission["resource_authority"], "plan_runtime");
8061        assert_eq!(admission["effective_max_concurrent"], 32);
8062        assert_eq!(admission["maximum_active_sequences"], 32);
8063        assert_eq!(admission["maximum_scheduled_tokens"], 4096);
8064        assert_eq!(admission["preflight_effective_max_concurrent"], 16);
8065        assert_eq!(admission["active_sequences"], 30);
8066        assert_eq!(admission["active_prefill"], 7);
8067        assert_eq!(admission["active_decode"], 23);
8068        assert!(admission["current_batch_size"].is_null());
8069        assert_eq!(admission["queue_depth"], 2);
8070        assert_eq!(admission["capacity_blocked_requests"], 3);
8071    }
8072
8073    #[test]
8074    fn admission_health_surfaces_runtime_contract_failure_without_preflight_fallback() {
8075        let engine_status = EngineStatus {
8076            is_ready: true,
8077            loaded_models: Vec::new(),
8078            active_requests: 32,
8079            queued_requests: 1,
8080            memory_usage: MemoryUsage {
8081                total_bytes: 0,
8082                used_bytes: 0,
8083                free_bytes: 0,
8084                gpu_memory_bytes: None,
8085                cpu_memory_bytes: None,
8086                cache_memory_bytes: 0,
8087                utilization_percent: 0.0,
8088            },
8089            uptime_seconds: 0,
8090            last_heartbeat: chrono::Utc::now(),
8091            version: "test".to_owned(),
8092        };
8093        let admission = admission_health_json(
8094            &engine_status,
8095            &EngineMetrics::default(),
8096            &json!({
8097                "admission": {
8098                    "effective_max_concurrent": 16,
8099                    "scheduler_policy": "continuous"
8100                }
8101            }),
8102            None,
8103            Some("active phase count exceeded the runtime ceiling"),
8104        );
8105
8106        assert_eq!(admission["source"], "runtime_error");
8107        assert_eq!(admission["runtime_snapshot_available"], false);
8108        assert_eq!(admission["preflight_effective_max_concurrent"], 16);
8109        assert!(admission["effective_max_concurrent"].is_null());
8110        assert!(admission["queue_depth"].is_null());
8111        assert_eq!(
8112            admission["runtime_contract_error"],
8113            "active phase count exceeded the runtime ceiling"
8114        );
8115    }
8116
8117    #[tokio::test]
8118    async fn route_health_includes_runtime_config_snapshot() {
8119        let response = get(router_with_stub("ok"), "/health").await;
8120        assert_eq!(response.status(), AxumStatusCode::OK);
8121        let body = response_json(response).await;
8122        assert_eq!(body["status"], "healthy");
8123        assert!(body["config"]["entries"].is_array(), "body: {body}");
8124        assert_eq!(body["auto_config"]["schema_version"], 1);
8125        assert!(body["auto_config"]["entries"].is_array(), "body: {body}");
8126        assert!(body["auto_config"]["admission"].is_object(), "body: {body}");
8127        assert_eq!(body["admission"]["schema_version"], 2);
8128        assert!(body["admission"]["effective_max_concurrent"].is_number());
8129        assert!(body["admission"]["queue_depth"].is_number());
8130        assert!(body["admission"]["active_sequences"].is_number());
8131        assert!(body["admission"]["active_prefill"].is_null());
8132        assert!(body["admission"]["active_decode"].is_null());
8133        assert!(body["admission"]["current_batch_size"].is_null());
8134        assert!(body["admission"]["rejected_requests_total"].is_number());
8135        assert!(body["admission"]["failed_requests_total"].is_number());
8136        assert!(body["admission"]["completed_requests_total"].is_number());
8137        assert!(body["admission"]["avg_queue_wait_time_ms"].is_number());
8138        assert!(body["scheduler"]["avg_wait_time_ms"].is_number());
8139        assert!(body["scheduler"]["scheduling_time_ms"].is_number());
8140        assert!(body["scheduler"]["model_execution_time_ms"].is_number());
8141        assert!(body["scheduler"]["iteration_lock_wait_time_ms"].is_number());
8142        assert!(
8143            body["auto_config"]["decisions"].is_array() || body["auto_config"]["error"].is_string(),
8144            "body: {body}"
8145        );
8146    }
8147
8148    #[tokio::test]
8149    async fn route_metrics_includes_admission_counters() {
8150        let response = get(router_with_stub("ok"), "/metrics").await;
8151        assert_eq!(response.status(), AxumStatusCode::OK);
8152        let body = response_text(response).await;
8153        for metric in [
8154            "ferrum_admission_runtime_snapshot_available",
8155            "ferrum_admission_effective_max_concurrent",
8156            "ferrum_admission_queue_depth",
8157            "ferrum_admission_active_sequences",
8158            "ferrum_admission_rejected_requests_total",
8159            "ferrum_admission_failed_requests_total",
8160            "ferrum_admission_completed_requests_total",
8161        ] {
8162            assert!(body.contains(metric), "missing {metric}:\n{body}");
8163        }
8164        for unavailable_metric in [
8165            "ferrum_admission_maximum_active_sequences ",
8166            "ferrum_admission_maximum_scheduled_tokens ",
8167            "ferrum_admission_capacity_blocked_requests ",
8168            "ferrum_admission_active_prefill ",
8169            "ferrum_admission_active_decode ",
8170            "ferrum_admission_current_batch_size ",
8171        ] {
8172            assert!(
8173                !body.contains(unavailable_metric),
8174                "unknown metric was encoded as a real value: {unavailable_metric}\n{body}"
8175            );
8176        }
8177    }
8178
8179    #[tokio::test]
8180    async fn route_health_includes_engine_lora_metrics_snapshot() {
8181        let router = AxumServer::from_llm(Arc::new(StubLlm::with_lora_metrics(
8182            "ok",
8183            json!({
8184                "enabled": true,
8185                "adapter_count": 1,
8186                "active_cache_bindings": 0,
8187                "projection_applications": 7,
8188                "position": "real-inference",
8189                "source": "test-lora",
8190            }),
8191        )))
8192        .with_lora_adapters(
8193            "stub-model",
8194            vec![LoraAdapterModel::new(
8195                "sql",
8196                "stub-model:sql",
8197                "/tmp/sql-adapter",
8198            )],
8199        )
8200        .unwrap()
8201        .build_router();
8202        let response = get(router, "/health").await;
8203        assert_eq!(response.status(), AxumStatusCode::OK);
8204        let body = response_json(response).await;
8205        assert_eq!(body["lora"]["enabled"], true);
8206        assert_eq!(body["lora"]["adapter_count"], 1);
8207        assert_eq!(body["lora"]["projection_applications"], 7);
8208        assert_eq!(body["lora"]["position"], "real-inference");
8209        assert_eq!(body["lora"]["source"], "test-lora");
8210    }
8211
8212    #[tokio::test]
8213    async fn route_health_includes_engine_execution_attribution_snapshot() {
8214        let router = AxumServer::from_llm(Arc::new(StubLlm::with_execution_attribution(
8215            "ok",
8216            json!({
8217                "schema": "ferrum.vnext.provider-attribution.v1",
8218                "attribution_basis": "resolved_plan_and_completed_static_initialization",
8219                "provider_attribution": {
8220                    "expected_quant_tensor_count": 400,
8221                    "attributed_quant_tensor_count": 400,
8222                    "expected_operation_count": 3,
8223                    "attributed_operation_count": 3,
8224                    "expected_item_count": 403,
8225                    "attributed_item_count": 403,
8226                    "percent": 100.0,
8227                    "denominator_sha256": "5e366997e15e1a94d90b1ae07281269e8a46f75904306564d56354c8ebea2e4e"
8228                },
8229                "fallback_counts": {"silent": 0, "dense": 0, "legacy": 0}
8230            }),
8231        )))
8232        .build_router();
8233        let response = get(router, "/health").await;
8234        assert_eq!(response.status(), AxumStatusCode::OK);
8235        let body = response_json(response).await;
8236        assert_eq!(
8237            body["execution_attribution"]["provider_attribution"]["expected_item_count"],
8238            403
8239        );
8240        assert_eq!(
8241            body["execution_attribution"]["provider_attribution"]["denominator_sha256"],
8242            "5e366997e15e1a94d90b1ae07281269e8a46f75904306564d56354c8ebea2e4e"
8243        );
8244        assert_eq!(
8245            body["execution_attribution"]["fallback_counts"],
8246            json!({"silent": 0, "dense": 0, "legacy": 0})
8247        );
8248    }
8249
8250    #[tokio::test]
8251    async fn route_models_lists_loaded_stub_model() {
8252        let response = get(router_with_stub("ok"), "/v1/models").await;
8253        assert_eq!(response.status(), AxumStatusCode::OK);
8254        let body = response_json(response).await;
8255        assert_eq!(body["object"], "list");
8256        let data = body["data"].as_array().expect("models data array");
8257        assert_eq!(data.len(), 1, "body: {body}");
8258        assert_eq!(data[0]["id"], "stub-model");
8259        assert_eq!(data[0]["object"], "model");
8260        assert_eq!(data[0]["owned_by"], "ferrum");
8261        assert!(data[0]["created"].as_u64().unwrap_or_default() > 0);
8262        assert_eq!(data[0]["modalities"], json!(["text"]));
8263        assert!(data[0]["permission"].as_array().unwrap().is_empty());
8264        assert!(data[0]["root"].is_null());
8265        assert!(data[0]["parent"].is_null());
8266    }
8267
8268    #[tokio::test]
8269    async fn route_chat_public_alias_maps_to_internal_model_and_is_echoed() {
8270        let engine = Arc::new(CapturingLlm::new());
8271        let registry = ServedModelRegistry::try_new(
8272            "qwen3",
8273            ServedModelKind::Llm,
8274            vec!["served-alias".to_string(), "secondary-alias".to_string()],
8275            vec![],
8276        )
8277        .unwrap();
8278        let router = AxumServer::from_llm(engine.clone())
8279            .with_served_model_registry(registry)
8280            .build_router();
8281        let response = post_json(
8282            router,
8283            "/v1/chat/completions",
8284            json!({
8285                "model": "secondary-alias",
8286                "messages": [{"role": "user", "content": "Say hi"}],
8287                "max_tokens": 8
8288            }),
8289        )
8290        .await;
8291
8292        assert_eq!(response.status(), AxumStatusCode::OK);
8293        let body = response_json(response).await;
8294        assert_eq!(body["model"], "secondary-alias");
8295        assert_eq!(engine.last_request().model_id, ModelId::new("qwen3"));
8296    }
8297
8298    #[tokio::test]
8299    async fn route_models_lists_public_aliases_without_internal_model_id() {
8300        let registry = ServedModelRegistry::try_new(
8301            "qwen3",
8302            ServedModelKind::Llm,
8303            vec!["served-alias".to_string(), "secondary-alias".to_string()],
8304            vec![],
8305        )
8306        .unwrap();
8307        let router = AxumServer::from_llm(Arc::new(CapturingLlm::new()))
8308            .with_served_model_registry(registry)
8309            .build_router();
8310        let body = response_json(get(router, "/v1/models").await).await;
8311        let ids = body["data"]
8312            .as_array()
8313            .unwrap()
8314            .iter()
8315            .map(|entry| entry["id"].as_str().unwrap())
8316            .collect::<Vec<_>>();
8317
8318        assert_eq!(ids, vec!["served-alias", "secondary-alias"]);
8319        assert!(!ids.contains(&"qwen3"));
8320        assert!(body["data"]
8321            .as_array()
8322            .unwrap()
8323            .iter()
8324            .all(|entry| entry["modalities"] == json!(["text"])));
8325    }
8326
8327    #[tokio::test]
8328    async fn route_models_lists_embedding_registry_capabilities() {
8329        let body = response_json(get(router_with_stub_embed(), "/v1/models").await).await;
8330        let data = body["data"].as_array().unwrap();
8331
8332        assert_eq!(data.len(), 1);
8333        assert_eq!(data[0]["id"], "stub-embed");
8334        assert_eq!(data[0]["modalities"], json!(["text", "image"]));
8335    }
8336
8337    #[tokio::test]
8338    async fn route_models_lists_startup_lora_adapters() {
8339        let router = AxumServer::from_llm(Arc::new(StubLlm::new("ok")))
8340            .with_lora_adapters(
8341                "stub-model",
8342                vec![LoraAdapterModel::new(
8343                    "sql",
8344                    "stub-model:sql",
8345                    "/tmp/sql-adapter",
8346                )],
8347            )
8348            .unwrap()
8349            .build_router();
8350        let response = get(router, "/v1/models").await;
8351        assert_eq!(response.status(), AxumStatusCode::OK);
8352        let body = response_json(response).await;
8353        let data = body["data"].as_array().expect("models data array");
8354        let ids: Vec<_> = data
8355            .iter()
8356            .map(|item| item["id"].as_str().unwrap_or_default())
8357            .collect();
8358        assert!(ids.contains(&"stub-model"), "body: {body}");
8359        assert!(ids.contains(&"stub-model:sql"), "body: {body}");
8360        let adapter = data
8361            .iter()
8362            .find(|item| item["id"] == "stub-model:sql")
8363            .expect("adapter model");
8364        assert_eq!(adapter["root"], "stub-model");
8365        assert_eq!(adapter["parent"], "stub-model");
8366        assert_eq!(adapter["modalities"], json!(["text"]));
8367    }
8368
8369    #[tokio::test]
8370    async fn route_chat_lora_adapter_maps_internal_request_to_base_model() {
8371        let (router, engine) = router_with_capturing_lora_llm();
8372        let response = post_json(
8373            router,
8374            "/v1/chat/completions",
8375            json!({
8376                "model": "qwen3:sql",
8377                "messages": [{"role": "user", "content": "Say hi"}],
8378                "max_tokens": 8,
8379                "temperature": 0.0
8380            }),
8381        )
8382        .await;
8383        assert_eq!(response.status(), AxumStatusCode::OK);
8384        let body = response_json(response).await;
8385        assert_eq!(body["model"], "qwen3:sql");
8386        let captured = engine.last_request();
8387        assert_eq!(captured.model_id, ModelId::new("qwen3"));
8388        assert_eq!(captured.metadata["ferrum_lora_adapter"], "sql");
8389        assert_eq!(captured.metadata["ferrum_lora_model_id"], "qwen3:sql");
8390    }
8391
8392    #[tokio::test]
8393    async fn route_chat_base_model_still_uses_base_path_with_lora_loaded() {
8394        let (router, engine) = router_with_capturing_lora_llm();
8395        let response = post_json(
8396            router,
8397            "/v1/chat/completions",
8398            json!({
8399                "model": "qwen3",
8400                "messages": [{"role": "user", "content": "Say hi"}],
8401                "max_tokens": 8,
8402                "temperature": 0.0
8403            }),
8404        )
8405        .await;
8406        assert_eq!(response.status(), AxumStatusCode::OK);
8407        let captured = engine.last_request();
8408        assert_eq!(captured.model_id, ModelId::new("qwen3"));
8409        assert!(!captured.metadata.contains_key("ferrum_lora_adapter"));
8410    }
8411
8412    #[tokio::test]
8413    async fn route_chat_unknown_lora_adapter_returns_openai_model_error() {
8414        let (router, _) = router_with_capturing_lora_llm();
8415        let response = post_json(
8416            router,
8417            "/v1/chat/completions",
8418            json!({
8419                "model": "qwen3:missing",
8420                "messages": [{"role": "user", "content": "Say hi"}],
8421                "max_tokens": 8
8422            }),
8423        )
8424        .await;
8425        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
8426        let body = response_json(response).await;
8427        assert_eq!(body["error"]["type"], "invalid_request_error");
8428        assert_eq!(body["error"]["param"], "model");
8429        assert!(
8430            body["error"]["message"]
8431                .as_str()
8432                .unwrap_or_default()
8433                .contains("unknown model"),
8434            "body: {body}"
8435        );
8436    }
8437
8438    #[tokio::test]
8439    async fn route_chat_unknown_served_model_returns_openai_model_error() {
8440        let engine = Arc::new(CapturingLlm::new());
8441        let router = AxumServer::from_llm(engine.clone()).build_router();
8442        let response = post_json(
8443            router,
8444            "/v1/chat/completions",
8445            json!({
8446                "model": "not-a-loaded-model",
8447                "messages": [{"role": "user", "content": "Say hi"}],
8448                "max_tokens": 8
8449            }),
8450        )
8451        .await;
8452        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
8453        let body = response_json(response).await;
8454        assert_eq!(body["error"]["type"], "invalid_request_error");
8455        assert_eq!(body["error"]["param"], "model");
8456        assert!(
8457            body["error"]["message"]
8458                .as_str()
8459                .unwrap_or_default()
8460                .contains("unknown model"),
8461            "body: {body}"
8462        );
8463        assert!(!engine.has_captured_request());
8464    }
8465
8466    #[tokio::test]
8467    async fn route_models_without_engine_returns_empty_list() {
8468        let response = get(router_without_llm(), "/v1/models").await;
8469        assert_eq!(response.status(), AxumStatusCode::OK);
8470        let body = response_json(response).await;
8471        assert_eq!(body["object"], "list");
8472        assert!(body["data"].as_array().unwrap().is_empty(), "body: {body}");
8473    }
8474
8475    #[tokio::test]
8476    async fn route_basic_chat_contract_uses_stub_engine() {
8477        let response = post_json(
8478            router_with_stub("hello"),
8479            "/v1/chat/completions",
8480            json!({
8481                "model": "stub-model",
8482                "messages": [{"role": "user", "content": "Say hi"}],
8483                "max_tokens": 8,
8484                "temperature": 0.0
8485            }),
8486        )
8487        .await;
8488        assert_eq!(response.status(), AxumStatusCode::OK);
8489        let body = response_json(response).await;
8490        assert_eq!(body["object"], "chat.completion");
8491        assert_eq!(body["choices"][0]["message"]["role"], "assistant");
8492        assert_eq!(body["choices"][0]["message"]["content"], "hello");
8493        assert_eq!(body["usage"]["prompt_tokens"], 7);
8494        assert_eq!(body["usage"]["completion_tokens"], 2);
8495    }
8496
8497    #[tokio::test]
8498    async fn route_chat_serializes_structured_tool_call_response() {
8499        let response = post_json(
8500            router_with_stub_api_response(
8501                "",
8502                ferrum_types::ApiResponse::Chat(ferrum_types::ApiChatResponse {
8503                    message: ferrum_types::ApiChatMessage {
8504                        role: ferrum_types::ApiMessageRole::Assistant,
8505                        content: String::new(),
8506                        name: None,
8507                        tool_calls: vec![ferrum_types::ApiToolCall {
8508                            id: "call_1".to_string(),
8509                            tool_type: "function".to_string(),
8510                            function: ferrum_types::ApiFunctionCall {
8511                                name: "weather".to_string(),
8512                                arguments: "{\"city\":\"Paris\"}".to_string(),
8513                            },
8514                        }],
8515                        tool_call_id: None,
8516                        function_call: None,
8517                    },
8518                    finish_reason: Some("tool_calls".to_string()),
8519                }),
8520            ),
8521            "/v1/chat/completions",
8522            json!({
8523                "model": "stub-model",
8524                "messages": [{"role": "user", "content": "Use the weather tool."}],
8525                "tools": [{
8526                    "type": "function",
8527                    "function": {
8528                        "name": "weather",
8529                        "parameters": {
8530                            "type": "object",
8531                            "properties": {"city": {"type": "string"}},
8532                            "required": ["city"]
8533                        }
8534                    }
8535                }],
8536                "tool_choice": "auto"
8537            }),
8538        )
8539        .await;
8540        assert_eq!(response.status(), AxumStatusCode::OK);
8541        let body = response_json(response).await;
8542        assert_eq!(body["choices"][0]["finish_reason"], "tool_calls");
8543        assert_eq!(
8544            body["choices"][0]["message"]["tool_calls"][0]["function"]["name"],
8545            "weather"
8546        );
8547        assert_eq!(
8548            body["choices"][0]["message"]["tool_calls"][0]["function"]["arguments"],
8549            "{\"city\":\"Paris\"}"
8550        );
8551    }
8552
8553    #[tokio::test]
8554    async fn route_chat_preserves_length_over_structured_tool_response() {
8555        let generated = r#"{"name":"weather","arguments":{"city":"Paris"}}"#;
8556        let response = post_json(
8557            router_with_stub_api_response_and_finish_reason(
8558                generated,
8559                weather_tool_api_response(),
8560                FinishReason::Length,
8561            ),
8562            "/v1/chat/completions",
8563            json!({
8564                "model": "stub-model",
8565                "messages": [{"role": "user", "content": "Use the weather tool."}],
8566                "tools": [{
8567                    "type": "function",
8568                    "function": {"name": "weather", "parameters": {"type": "object"}}
8569                }],
8570                "tool_choice": "auto"
8571            }),
8572        )
8573        .await;
8574        assert_eq!(response.status(), AxumStatusCode::OK);
8575        let body = response_json(response).await;
8576        assert_eq!(body["choices"][0]["finish_reason"], "length");
8577        assert_eq!(body["choices"][0]["message"]["content"], generated);
8578        assert!(body["choices"][0]["message"]["tool_calls"].is_null());
8579    }
8580
8581    #[tokio::test]
8582    async fn route_chat_serializes_generated_tool_call_json_when_engine_returns_text_only() {
8583        let response = post_json(
8584            router_with_stub(
8585                r#"{"tool_calls":[{"id":"call_1","type":"function","function":{"name":"weather","arguments":{"city":"Paris"}}}]}"#,
8586            ),
8587            "/v1/chat/completions",
8588            json!({
8589                "model": "stub-model",
8590                "messages": [{"role": "user", "content": "Use the weather tool."}],
8591                "tools": [{
8592                    "type": "function",
8593                    "function": {
8594                        "name": "weather",
8595                        "parameters": {
8596                            "type": "object",
8597                            "properties": {"city": {"type": "string"}},
8598                            "required": ["city"]
8599                        }
8600                    }
8601                }],
8602                "tool_choice": "auto"
8603            }),
8604        )
8605        .await;
8606        assert_eq!(response.status(), AxumStatusCode::OK);
8607        let body = response_json(response).await;
8608        assert_eq!(body["choices"][0]["finish_reason"], "tool_calls");
8609        assert_eq!(body["choices"][0]["message"]["content"], "");
8610        assert_eq!(
8611            body["choices"][0]["message"]["tool_calls"][0]["id"],
8612            "call_1"
8613        );
8614        assert_eq!(
8615            body["choices"][0]["message"]["tool_calls"][0]["function"]["name"],
8616            "weather"
8617        );
8618        assert_eq!(
8619            body["choices"][0]["message"]["tool_calls"][0]["function"]["arguments"],
8620            "{\"city\":\"Paris\"}"
8621        );
8622    }
8623
8624    #[tokio::test]
8625    async fn route_chat_serializes_qwen3_function_parameters_tool_json() {
8626        let response = post_json(
8627            router_with_stub(
8628                r#"{"function":"get_weather","parameters":{"city":"北京","unit":"c"}}"#,
8629            ),
8630            "/v1/chat/completions",
8631            json!({
8632                "model": "stub-model",
8633                "messages": [{"role": "user", "content": "北京现在天气怎么样?"}],
8634                "tools": [{
8635                    "type": "function",
8636                    "function": {
8637                        "name": "get_weather",
8638                        "parameters": {
8639                            "type": "object",
8640                            "properties": {
8641                                "city": {"type": "string"},
8642                                "unit": {"type": "string", "enum": ["c", "f"]}
8643                            },
8644                            "required": ["city"]
8645                        }
8646                    }
8647                }],
8648                "tool_choice": "auto"
8649            }),
8650        )
8651        .await;
8652        assert_eq!(response.status(), AxumStatusCode::OK);
8653        let body = response_json(response).await;
8654        assert_eq!(body["choices"][0]["finish_reason"], "tool_calls");
8655        assert_eq!(body["choices"][0]["message"]["content"], "");
8656        assert_eq!(
8657            body["choices"][0]["message"]["tool_calls"][0]["function"]["name"],
8658            "get_weather"
8659        );
8660        assert_eq!(
8661            body["choices"][0]["message"]["tool_calls"][0]["function"]["arguments"],
8662            "{\"city\":\"北京\",\"unit\":\"c\"}"
8663        );
8664    }
8665
8666    #[tokio::test]
8667    async fn route_chat_uses_template_tool_protocol_for_function_parameter_xml() {
8668        let template = ModelChatTemplate::new(
8669            "{% if tools %}<tools>{{ tools | tojson }}</tools>Use <tool_call><function=name><parameter=key>value</parameter></function></tool_call>{% endif %}{% for message in messages %}{{ message.content }}{% endfor %}{% if add_generation_prompt %}<assistant>{% endif %}",
8670            "function-parameter-xml-template",
8671        );
8672        let response = post_json(
8673            router_with_stub_and_template(
8674                "<tool_call>\n<function=get_weather>\n<parameter=city>\n北京\n</parameter>\n<parameter=unit>\ncelsius\n</parameter>\n</function>\n</tool_call>",
8675                template,
8676            ),
8677            "/v1/chat/completions",
8678            json!({
8679                "model": "stub-model",
8680                "messages": [{"role": "user", "content": "请调用 get_weather 查询北京天气。"}],
8681                "tools": [{
8682                    "type": "function",
8683                    "function": {
8684                        "name": "get_weather",
8685                        "parameters": {
8686                            "type": "object",
8687                            "properties": {
8688                                "city": {"type": "string"},
8689                                "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
8690                            },
8691                            "required": ["city"]
8692                        }
8693                    }
8694                }]
8695            }),
8696        )
8697        .await;
8698
8699        assert_eq!(response.status(), AxumStatusCode::OK);
8700        let body = response_json(response).await;
8701        assert_eq!(body["choices"][0]["finish_reason"], "tool_calls");
8702        assert_eq!(
8703            body["choices"][0]["message"]["tool_calls"][0]["function"]["name"],
8704            "get_weather"
8705        );
8706        assert_eq!(
8707            body["choices"][0]["message"]["tool_calls"][0]["function"]["arguments"],
8708            "{\"city\":\"北京\",\"unit\":\"celsius\"}"
8709        );
8710    }
8711
8712    fn xml_object_argument_tool_request(stream: bool) -> Value {
8713        json!({
8714            "model": "stub-model",
8715            "messages": [{"role": "user", "content": "Weather in Berlin with a forecast."}],
8716            "stream": stream,
8717            "tools": [{
8718                "type": "function",
8719                "function": {
8720                    "name": "get_weather",
8721                    "parameters": {
8722                        "type": "object",
8723                        "$defs": {
8724                            "WeatherOptions": {
8725                                "type": "object",
8726                                "properties": {
8727                                    "unit": {"type": "string"},
8728                                    "include_forecast": {"type": "boolean"}
8729                                },
8730                                "required": ["unit", "include_forecast"],
8731                                "additionalProperties": false
8732                            }
8733                        },
8734                        "properties": {
8735                            "city": {"type": "string"},
8736                            "options": {"$ref": "#/$defs/WeatherOptions"}
8737                        },
8738                        "required": ["city", "options"],
8739                        "additionalProperties": false
8740                    }
8741                }
8742            }]
8743        })
8744    }
8745
8746    #[tokio::test]
8747    async fn route_chat_decodes_xml_object_argument_through_local_schema_ref() {
8748        let template = ModelChatTemplate::new(
8749            "{% if tools %}<tool_call><function=name><parameter=key>value</parameter></function></tool_call>{% endif %}{% for message in messages %}{{ message.content }}{% endfor %}",
8750            "function-parameter-xml-template",
8751        );
8752        let response = post_json(
8753            router_with_stub_and_template(
8754                concat!(
8755                    "<tool_call>\n",
8756                    "<function=get_weather>\n",
8757                    "<parameter=city>\nBerlin\n</parameter>\n",
8758                    "<parameter=options>\n",
8759                    "{\"unit\":\"celsius\",\"include_forecast\":true}\n",
8760                    "</parameter>\n",
8761                    "</function>\n",
8762                    "</tool_call>",
8763                ),
8764                template,
8765            ),
8766            "/v1/chat/completions",
8767            xml_object_argument_tool_request(false),
8768        )
8769        .await;
8770
8771        assert_eq!(response.status(), AxumStatusCode::OK);
8772        let body = response_json(response).await;
8773        assert_eq!(body["choices"][0]["finish_reason"], "tool_calls");
8774        let arguments = body["choices"][0]["message"]["tool_calls"][0]["function"]["arguments"]
8775            .as_str()
8776            .and_then(|arguments| serde_json::from_str::<Value>(arguments).ok())
8777            .expect("tool arguments must contain one-decode structured JSON");
8778        assert_eq!(arguments["city"], json!("Berlin"));
8779        assert_eq!(
8780            arguments["options"],
8781            json!({"unit": "celsius", "include_forecast": true})
8782        );
8783    }
8784
8785    #[tokio::test]
8786    async fn route_chat_rejects_malformed_native_xml_object_argument() {
8787        let template = ModelChatTemplate::new(
8788            "{% if tools %}<tool_call><function=name><parameter=key>value</parameter></function></tool_call>{% endif %}{% for message in messages %}{{ message.content }}{% endfor %}",
8789            "function-parameter-xml-template",
8790        );
8791        let response = post_json(
8792            router_with_stub_and_template(
8793                concat!(
8794                    "<tool_call><function=get_weather>",
8795                    "<parameter=city>Berlin</parameter>",
8796                    "<parameter=options>{\"unit\":\"celsius\",</parameter>",
8797                    "</function></tool_call>",
8798                ),
8799                template,
8800            ),
8801            "/v1/chat/completions",
8802            xml_object_argument_tool_request(false),
8803        )
8804        .await;
8805
8806        assert_eq!(response.status(), AxumStatusCode::INTERNAL_SERVER_ERROR);
8807        let body = response_json(response).await;
8808        assert_eq!(body["error"]["type"], "internal_server_error");
8809        assert!(
8810            body["error"]["message"]
8811                .as_str()
8812                .is_some_and(|message| message.contains("did not satisfy its schema")),
8813            "body: {body}"
8814        );
8815    }
8816
8817    #[tokio::test]
8818    async fn route_streaming_chat_rejects_malformed_native_xml_before_tool_delta() {
8819        let template = ModelChatTemplate::new(
8820            "{% if tools %}<tool_call><function=name><parameter=key>value</parameter></function></tool_call>{% endif %}{% for message in messages %}{{ message.content }}{% endfor %}",
8821            "function-parameter-xml-template",
8822        );
8823        let response = post_json(
8824            router_with_stub_and_template(
8825                concat!(
8826                    "<tool_call><function=get_weather>",
8827                    "<parameter=city>Berlin</parameter>",
8828                    "<parameter=options>{\"unit\":\"celsius\",</parameter>",
8829                    "</function></tool_call>",
8830                ),
8831                template,
8832            ),
8833            "/v1/chat/completions",
8834            xml_object_argument_tool_request(true),
8835        )
8836        .await;
8837
8838        assert_eq!(response.status(), AxumStatusCode::OK);
8839        let body = response_text(response).await;
8840        assert_eq!(body.matches("data: [DONE]").count(), 1, "body: {body}");
8841        assert!(
8842            body.contains(r#""error":{"#) && body.contains("did not satisfy its schema"),
8843            "stream must return a controlled schema error: {body}"
8844        );
8845        assert!(
8846            !body.contains(r#""tool_calls":[{"#),
8847            "invalid native arguments must not leak a tool delta: {body}"
8848        );
8849    }
8850
8851    #[tokio::test]
8852    async fn route_chat_parses_tool_call_from_reasoning_before_fake_tool_result_content() {
8853        let response = post_json(
8854            router_with_stub(
8855                "kaza\n\
8856                 {\"name\":\"get_weather\",\"arguments\":{\"city\":\"北京\",\"unit\":\"celsius\"}}\n\
8857                 </think>\n\
8858                 {\"name\":\"get_weather\",\"content\":{\"temperature\":25,\"condition\":\"晴\"}}\n\
8859                 {\"temperature\":25,\"condition\":\"晴\"}",
8860            ),
8861            "/v1/chat/completions",
8862            json!({
8863                "model": "stub-model",
8864                "messages": [{"role": "user", "content": "北京现在天气怎么样?请先调用工具。"}],
8865                "tools": [{
8866                    "type": "function",
8867                    "function": {
8868                        "name": "get_weather",
8869                        "parameters": {
8870                            "type": "object",
8871                            "properties": {
8872                                "city": {"type": "string"},
8873                                "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
8874                            },
8875                            "required": ["city"]
8876                        }
8877                    }
8878                }]
8879            }),
8880        )
8881        .await;
8882        assert_eq!(response.status(), AxumStatusCode::OK);
8883        let body = response_json(response).await;
8884        assert_eq!(body["choices"][0]["finish_reason"], "tool_calls");
8885        assert_eq!(body["choices"][0]["message"]["content"], "");
8886        assert_eq!(
8887            body["choices"][0]["message"]["tool_calls"][0]["function"]["name"],
8888            "get_weather"
8889        );
8890        assert_eq!(
8891            body["choices"][0]["message"]["tool_calls"][0]["function"]["arguments"],
8892            "{\"city\":\"北京\",\"unit\":\"celsius\"}"
8893        );
8894    }
8895
8896    #[tokio::test]
8897    async fn route_chat_prefers_reasoning_tool_call_over_empty_visible_arguments() {
8898        let response = post_json(
8899            router_with_stub(
8900                "{\"name\":\"get_weather\",\"arguments\":{\"city\":\"北京\",\"unit\":\"celsius\"}}\n\
8901                 </think>\n\
8902                 {\"name\":\"get_weather\",\"arguments\":{}}",
8903            ),
8904            "/v1/chat/completions",
8905            json!({
8906                "model": "stub-model",
8907                "messages": [{"role": "user", "content": "北京现在天气怎么样?请先调用 get_weather 工具。"}],
8908                "tools": [{
8909                    "type": "function",
8910                    "function": {
8911                        "name": "get_weather",
8912                        "parameters": {
8913                            "type": "object",
8914                            "properties": {
8915                                "city": {"type": "string"},
8916                                "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
8917                            },
8918                            "required": ["city"]
8919                        }
8920                    }
8921                }]
8922            }),
8923        )
8924        .await;
8925        assert_eq!(response.status(), AxumStatusCode::OK);
8926        let body = response_json(response).await;
8927        assert_eq!(body["choices"][0]["finish_reason"], "tool_calls");
8928        assert_eq!(
8929            body["choices"][0]["message"]["tool_calls"][0]["function"]["name"],
8930            "get_weather"
8931        );
8932        assert_eq!(
8933            body["choices"][0]["message"]["tool_calls"][0]["function"]["arguments"],
8934            "{\"city\":\"北京\",\"unit\":\"celsius\"}"
8935        );
8936    }
8937
8938    #[tokio::test]
8939    async fn route_chat_honors_specific_tool_choice_for_generated_tool_call_json() {
8940        let response = post_json(
8941            router_with_stub(r#"{"name":"weather","arguments":{"city":"Paris"}}"#),
8942            "/v1/chat/completions",
8943            json!({
8944                "model": "stub-model",
8945                "messages": [{"role": "user", "content": "Use the selected tool."}],
8946                "tools": [
8947                    {
8948                        "type": "function",
8949                        "function": {"name": "weather", "parameters": {"type": "object"}}
8950                    },
8951                    {
8952                        "type": "function",
8953                        "function": {"name": "calendar", "parameters": {"type": "object"}}
8954                    }
8955                ],
8956                "tool_choice": {
8957                    "type": "function",
8958                    "function": {"name": "weather"}
8959                }
8960            }),
8961        )
8962        .await;
8963        assert_eq!(response.status(), AxumStatusCode::OK);
8964        let body = response_json(response).await;
8965        assert_eq!(body["choices"][0]["finish_reason"], "tool_calls");
8966        assert_eq!(
8967            body["choices"][0]["message"]["tool_calls"][0]["function"]["name"],
8968            "weather"
8969        );
8970
8971        let response = post_json(
8972            router_with_stub(r#"{"name":"calendar","arguments":{}}"#),
8973            "/v1/chat/completions",
8974            json!({
8975                "model": "stub-model",
8976                "messages": [{"role": "user", "content": "Use the selected tool."}],
8977                "tools": [
8978                    {
8979                        "type": "function",
8980                        "function": {"name": "weather", "parameters": {"type": "object"}}
8981                    },
8982                    {
8983                        "type": "function",
8984                        "function": {"name": "calendar", "parameters": {"type": "object"}}
8985                    }
8986                ],
8987                "tool_choice": {
8988                    "type": "function",
8989                    "function": {"name": "weather"}
8990                }
8991            }),
8992        )
8993        .await;
8994        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
8995        let body = response_json(response).await;
8996        assert_eq!(body["error"]["param"], "tool_choice");
8997        assert_eq!(body["error"]["type"], "invalid_request_error");
8998    }
8999
9000    #[tokio::test]
9001    async fn route_chat_specific_tool_choice_wraps_generated_arguments() {
9002        let response = post_json(
9003            router_with_stub(r#"{"city":"Paris"}"#),
9004            "/v1/chat/completions",
9005            json!({
9006                "model": "stub-model",
9007                "messages": [{"role": "user", "content": "Use the selected tool."}],
9008                "tools": [{
9009                    "type": "function",
9010                    "function": {
9011                        "name": "weather",
9012                        "parameters": {
9013                            "type": "object",
9014                            "properties": {"city": {"type": "string"}},
9015                            "required": ["city"]
9016                        }
9017                    }
9018                }],
9019                "tool_choice": {
9020                    "type": "function",
9021                    "function": {"name": "weather"}
9022                }
9023            }),
9024        )
9025        .await;
9026        assert_eq!(response.status(), AxumStatusCode::OK);
9027        let body = response_json(response).await;
9028        assert_eq!(body["choices"][0]["finish_reason"], "tool_calls");
9029        assert_eq!(body["choices"][0]["message"]["content"], "");
9030        assert_eq!(
9031            body["choices"][0]["message"]["tool_calls"][0]["function"]["name"],
9032            "weather"
9033        );
9034        assert_eq!(
9035            body["choices"][0]["message"]["tool_calls"][0]["function"]["arguments"],
9036            "{\"city\":\"Paris\"}"
9037        );
9038    }
9039
9040    #[tokio::test]
9041    async fn route_chat_tool_choice_none_keeps_generated_tool_json_as_content() {
9042        let generated = r#"{"name":"weather","arguments":{"city":"Paris"}}"#;
9043        let response = post_json(
9044            router_with_stub(generated),
9045            "/v1/chat/completions",
9046            json!({
9047                "model": "stub-model",
9048                "messages": [{"role": "user", "content": "Do not use tools."}],
9049                "tools": [{
9050                    "type": "function",
9051                    "function": {"name": "weather", "parameters": {"type": "object"}}
9052                }],
9053                "tool_choice": "none"
9054            }),
9055        )
9056        .await;
9057        assert_eq!(response.status(), AxumStatusCode::OK);
9058        let body = response_json(response).await;
9059        assert_eq!(body["choices"][0]["finish_reason"], "stop");
9060        assert_eq!(body["choices"][0]["message"]["content"], generated);
9061        assert!(body["choices"][0]["message"]["tool_calls"].is_null());
9062    }
9063
9064    #[tokio::test]
9065    async fn route_chat_tool_choice_required_wraps_generated_arguments() {
9066        let response = post_json(
9067            router_with_stub(r#"{"city":"Paris"}"#),
9068            "/v1/chat/completions",
9069            json!({
9070                "model": "stub-model",
9071                "messages": [{"role": "user", "content": "Use a tool."}],
9072                "tools": [{
9073                    "type": "function",
9074                    "function": {
9075                        "name": "weather",
9076                        "parameters": {
9077                            "type": "object",
9078                            "properties": {"city": {"type": "string"}},
9079                            "required": ["city"]
9080                        }
9081                    }
9082                }],
9083                "tool_choice": "required"
9084            }),
9085        )
9086        .await;
9087        assert_eq!(response.status(), AxumStatusCode::OK);
9088        let body = response_json(response).await;
9089        assert_eq!(body["choices"][0]["finish_reason"], "tool_calls");
9090        assert_eq!(body["choices"][0]["message"]["content"], "");
9091        assert_eq!(
9092            body["choices"][0]["message"]["tool_calls"][0]["function"]["name"],
9093            "weather"
9094        );
9095        assert_eq!(
9096            body["choices"][0]["message"]["tool_calls"][0]["function"]["arguments"],
9097            "{\"city\":\"Paris\"}"
9098        );
9099    }
9100
9101    fn required_tool_with_strict_response_format_request(stream: bool) -> Value {
9102        json!({
9103            "model": "stub-model",
9104            "messages": [{"role": "user", "content": "Use the weather tool."}],
9105            "stream": stream,
9106            "stream_options": stream.then_some(json!({"include_usage": true})),
9107            "tools": [{
9108                "type": "function",
9109                "function": {
9110                    "name": "weather",
9111                    "parameters": {
9112                        "type": "object",
9113                        "properties": {"city": {"type": "string", "const": "Paris"}},
9114                        "required": ["city"],
9115                        "additionalProperties": false
9116                    }
9117                }
9118            }],
9119            "tool_choice": "required",
9120            "response_format": {
9121                "type": "json_schema",
9122                "json_schema": {
9123                    "name": "content_answer",
9124                    "strict": true,
9125                    "schema": {
9126                        "type": "object",
9127                        "properties": {"answer": {"type": "string", "const": "IGNORED"}},
9128                        "required": ["answer"],
9129                        "additionalProperties": false
9130                    }
9131                }
9132            }
9133        })
9134    }
9135
9136    #[tokio::test]
9137    async fn route_chat_required_tool_takes_priority_over_strict_response_format() {
9138        let response = post_json(
9139            router_with_stub(r#"{"city":"Paris"}"#),
9140            "/v1/chat/completions",
9141            required_tool_with_strict_response_format_request(false),
9142        )
9143        .await;
9144        assert_eq!(response.status(), AxumStatusCode::OK);
9145        let body = response_json(response).await;
9146        assert_eq!(body["choices"][0]["finish_reason"], "tool_calls");
9147        assert_eq!(body["choices"][0]["message"]["content"], "");
9148        assert_eq!(
9149            body["choices"][0]["message"]["tool_calls"][0]["function"]["name"],
9150            "weather"
9151        );
9152        assert_eq!(
9153            body["choices"][0]["message"]["tool_calls"][0]["function"]["arguments"],
9154            r#"{"city":"Paris"}"#
9155        );
9156    }
9157
9158    #[tokio::test]
9159    async fn route_chat_required_tool_rejects_arguments_that_violate_const_schema() {
9160        let response = post_json(
9161            router_with_stub(r#"{"city":"London"}"#),
9162            "/v1/chat/completions",
9163            required_tool_with_strict_response_format_request(false),
9164        )
9165        .await;
9166        assert_eq!(response.status(), AxumStatusCode::INTERNAL_SERVER_ERROR);
9167        let body = response_json(response).await;
9168        assert_eq!(body["error"]["type"], "internal_server_error");
9169        assert!(
9170            body["error"]["message"]
9171                .as_str()
9172                .is_some_and(|message| message.contains("did not satisfy its schema")),
9173            "body: {body}"
9174        );
9175    }
9176
9177    #[tokio::test]
9178    async fn route_streaming_required_tool_takes_priority_over_strict_response_format() {
9179        let response = post_json(
9180            router_with_stub(r#"{"city":"Paris"}"#),
9181            "/v1/chat/completions",
9182            required_tool_with_strict_response_format_request(true),
9183        )
9184        .await;
9185        assert_eq!(response.status(), AxumStatusCode::OK);
9186        let body = response_text(response).await;
9187        assert_eq!(body.matches("data: [DONE]").count(), 1, "body: {body}");
9188        assert!(
9189            body.contains(r#""finish_reason":"tool_calls""#),
9190            "tool priority must finish with tool_calls: {body}"
9191        );
9192        assert!(
9193            body.contains(r#""name":"weather""#)
9194                && body.contains(r#""arguments":"{\"city\":\"Paris\"}""#),
9195            "stream must carry the reconstructed tool call: {body}"
9196        );
9197        assert_eq!(
9198            body.matches(r#""usage":{"#).count(),
9199            1,
9200            "stream must carry exactly one usage row: {body}"
9201        );
9202        assert!(
9203            !body.contains("strict json_schema") && !body.contains("invalid JSON"),
9204            "dormant content schema must not reject a required tool call: {body}"
9205        );
9206    }
9207
9208    #[tokio::test]
9209    async fn dropping_buffered_http_response_drops_the_engine_stream() {
9210        let stream_dropped = Arc::new(Notify::new());
9211        let response = post_json(
9212            AxumServer::from_llm(Arc::new(StubLlm::with_pending_stream(Arc::clone(
9213                &stream_dropped,
9214            ))))
9215            .build_router(),
9216            "/v1/chat/completions",
9217            required_tool_with_strict_response_format_request(true),
9218        )
9219        .await;
9220        assert_eq!(response.status(), AxumStatusCode::OK);
9221
9222        drop(response);
9223        tokio::time::timeout(std::time::Duration::from_secs(1), stream_dropped.notified())
9224            .await
9225            .expect("client disconnect must stop a buffered structured stream promptly");
9226    }
9227
9228    #[tokio::test]
9229    async fn route_chat_tool_choice_required_errors_without_valid_tool_call() {
9230        let response = post_json(
9231            router_with_stub("plain answer"),
9232            "/v1/chat/completions",
9233            json!({
9234                "model": "stub-model",
9235                "messages": [{"role": "user", "content": "Use a tool."}],
9236                "tools": [{
9237                    "type": "function",
9238                    "function": {"name": "weather", "parameters": {"type": "object"}}
9239                }],
9240                "tool_choice": "required"
9241            }),
9242        )
9243        .await;
9244        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
9245        let body = response_json(response).await;
9246        assert_eq!(body["error"]["type"], "invalid_request_error");
9247        assert_eq!(body["error"]["param"], "tool_choice");
9248        assert!(
9249            body["error"]["message"]
9250                .as_str()
9251                .is_some_and(|message| message.contains("required tool_choice")),
9252            "body: {body}"
9253        );
9254    }
9255
9256    #[tokio::test]
9257    async fn route_streaming_chat_serializes_generated_tool_call_delta() {
9258        let response = post_json(
9259            router_with_stub(
9260                r#"{"tool_calls":[{"id":"call_1","type":"function","function":{"name":"weather","arguments":{"city":"Paris"}}}]}"#,
9261            ),
9262            "/v1/chat/completions",
9263            json!({
9264                "model": "stub-model",
9265                "messages": [{"role": "user", "content": "Use the weather tool."}],
9266                "stream": true,
9267                "tools": [{
9268                    "type": "function",
9269                    "function": {
9270                        "name": "weather",
9271                        "parameters": {
9272                            "type": "object",
9273                            "properties": {"city": {"type": "string"}},
9274                            "required": ["city"]
9275                        }
9276                    }
9277                }],
9278                "tool_choice": "auto"
9279            }),
9280        )
9281        .await;
9282        assert_eq!(response.status(), AxumStatusCode::OK);
9283        let body = response_text(response).await;
9284        assert!(body.contains("data: [DONE]"), "missing DONE: {body}");
9285        assert!(
9286            body.contains(r#""finish_reason":"tool_calls""#),
9287            "stream should finish with tool_calls: {body}"
9288        );
9289        assert!(
9290            body.contains(r#""tool_calls":[{"index":0,"id":"call_1","type":"function","function":{"name":"weather""#),
9291            "stream should emit OpenAI tool_calls delta with index: {body}"
9292        );
9293        assert!(
9294            body.contains(r#""arguments":"{\"city\":\"Paris\"}""#),
9295            "tool arguments should be serialized as JSON string: {body}"
9296        );
9297        assert!(
9298            !body.contains(r#""content":"{\"tool_calls\""#),
9299            "raw tool-call JSON should not be streamed as assistant content: {body}"
9300        );
9301    }
9302
9303    #[tokio::test]
9304    async fn route_streaming_chat_serializes_qwen3_function_parameters_tool_delta() {
9305        let response = post_json(
9306            router_with_stub(
9307                r#"{"function":"get_weather","parameters":{"city":"深圳","unit":"c"}}"#,
9308            ),
9309            "/v1/chat/completions",
9310            json!({
9311                "model": "stub-model",
9312                "messages": [{"role": "user", "content": "深圳天气?"}],
9313                "stream": true,
9314                "tools": [{
9315                    "type": "function",
9316                    "function": {
9317                        "name": "get_weather",
9318                        "parameters": {
9319                            "type": "object",
9320                            "properties": {
9321                                "city": {"type": "string"},
9322                                "unit": {"type": "string", "enum": ["c", "f"]}
9323                            },
9324                            "required": ["city"]
9325                        }
9326                    }
9327                }],
9328                "tool_choice": "auto"
9329            }),
9330        )
9331        .await;
9332        assert_eq!(response.status(), AxumStatusCode::OK);
9333        let body = response_text(response).await;
9334        assert!(body.contains("data: [DONE]"), "missing DONE: {body}");
9335        assert!(
9336            body.contains(r#""finish_reason":"tool_calls""#),
9337            "stream should finish with tool_calls: {body}"
9338        );
9339        assert!(
9340            body.contains(r#""function":{"name":"get_weather","arguments":"{\"city\":\"深圳\",\"unit\":\"c\"}"}"#),
9341            "stream should emit parsed Qwen3 function parameters as tool args: {body}"
9342        );
9343        assert!(
9344            !body.contains(r#""content":"{\"function\""#),
9345            "raw Qwen3 tool JSON should not leak as assistant content: {body}"
9346        );
9347    }
9348
9349    #[tokio::test]
9350    async fn route_streaming_chat_preserves_opencode_edit_xml_whitespace() {
9351        let template = ModelChatTemplate::new(
9352            "{% if tools %}<tool_call><function=name><parameter=key>value</parameter></function></tool_call>{% endif %}{% for message in messages %}{{ message.content }}{% endfor %}",
9353            "function-parameter-xml-template",
9354        );
9355        let generated = concat!(
9356            "<tool_call>\n",
9357            "<function=edit>\n",
9358            "<parameter=filePath>\n",
9359            "/workspace/src/main.rs\n",
9360            "</parameter>\n",
9361            "<parameter=oldString>\n",
9362            "    if x:\n",
9363            "        return 1\n",
9364            "\n",
9365            "</parameter>\n",
9366            "<parameter=newString>\n",
9367            "    if x:\n",
9368            "        return 2\n",
9369            "\n",
9370            "</parameter>\n",
9371            "<parameter=replaceAll>\n",
9372            "true\n",
9373            "</parameter>\n",
9374            "</function>\n",
9375            "</tool_call>",
9376        );
9377        let response = post_json(
9378            router_with_stub_and_template(generated, template),
9379            "/v1/chat/completions",
9380            json!({
9381                "model": "stub-model",
9382                "messages": [{"role": "user", "content": "Replace the code."}],
9383                "stream": true,
9384                "tools": [{
9385                    "type": "function",
9386                    "function": {
9387                        "name": "edit",
9388                        "parameters": {
9389                            "type": "object",
9390                            "properties": {
9391                                "filePath": {"type": "string"},
9392                                "oldString": {"type": "string"},
9393                                "newString": {"type": "string"},
9394                                "replaceAll": {"type": "boolean"}
9395                            },
9396                            "required": ["filePath", "oldString", "newString"]
9397                        }
9398                    }
9399                }]
9400            }),
9401        )
9402        .await;
9403
9404        assert_eq!(response.status(), AxumStatusCode::OK);
9405        let body = response_text(response).await;
9406        assert!(body.contains("data: [DONE]"), "missing DONE: {body}");
9407        assert!(
9408            body.contains(r#"\"oldString\":\"    if x:\\n        return 1\\n\""#),
9409            "stream must preserve exact code whitespace in tool arguments: {body}"
9410        );
9411        assert!(
9412            body.contains(r#"\"replaceAll\":true"#),
9413            "stream must preserve the boolean tool argument type: {body}"
9414        );
9415    }
9416
9417    #[tokio::test]
9418    async fn route_streaming_chat_honors_specific_tool_choice_for_generated_tool_call_delta() {
9419        let request = |generated: &'static str| {
9420            post_json(
9421                router_with_stub(generated),
9422                "/v1/chat/completions",
9423                json!({
9424                    "model": "stub-model",
9425                    "messages": [{"role": "user", "content": "Use the selected tool."}],
9426                    "stream": true,
9427                    "tools": [
9428                        {
9429                            "type": "function",
9430                            "function": {"name": "weather", "parameters": {"type": "object"}}
9431                        },
9432                        {
9433                            "type": "function",
9434                            "function": {"name": "calendar", "parameters": {"type": "object"}}
9435                        }
9436                    ],
9437                    "tool_choice": {
9438                        "type": "function",
9439                        "function": {"name": "weather"}
9440                    }
9441                }),
9442            )
9443        };
9444
9445        let response = request(r#"{"name":"weather","arguments":{"city":"Paris"}}"#).await;
9446        assert_eq!(response.status(), AxumStatusCode::OK);
9447        let body = response_text(response).await;
9448        assert!(body.contains("data: [DONE]"), "missing DONE: {body}");
9449        assert!(
9450            body.contains(r#""finish_reason":"tool_calls""#),
9451            "selected tool should finish with tool_calls: {body}"
9452        );
9453        assert!(
9454            body.contains(r#""function":{"name":"weather","arguments":"{\"city\":\"Paris\"}"}"#),
9455            "selected tool should stream as tool_calls delta: {body}"
9456        );
9457
9458        let response = request(r#"{"name":"calendar","arguments":{}}"#).await;
9459        assert_eq!(response.status(), AxumStatusCode::OK);
9460        let body = response_text(response).await;
9461        assert!(
9462            body.contains(
9463                r#""error":{"message":"model output did not satisfy required tool_choice""#
9464            ),
9465            "selected-tool stream should reject unselected tool output: {body}"
9466        );
9467        assert!(
9468            !body.contains(r#""finish_reason":"tool_calls""#),
9469            "unselected tool JSON must not become tool_calls: {body}"
9470        );
9471    }
9472
9473    #[tokio::test]
9474    async fn route_streaming_chat_prefers_chunk_api_response_for_tool_delta() {
9475        let response = post_json(
9476            router_with_stub_api_response(
9477                "raw text that should not stream",
9478                ferrum_types::ApiResponse::Chat(ferrum_types::ApiChatResponse {
9479                    message: ferrum_types::ApiChatMessage {
9480                        role: ferrum_types::ApiMessageRole::Assistant,
9481                        content: String::new(),
9482                        name: None,
9483                        tool_calls: vec![ferrum_types::ApiToolCall {
9484                            id: "call_1".to_string(),
9485                            tool_type: "function".to_string(),
9486                            function: ferrum_types::ApiFunctionCall {
9487                                name: "weather".to_string(),
9488                                arguments: "{\"city\":\"Paris\"}".to_string(),
9489                            },
9490                        }],
9491                        tool_call_id: None,
9492                        function_call: None,
9493                    },
9494                    finish_reason: Some("tool_calls".to_string()),
9495                }),
9496            ),
9497            "/v1/chat/completions",
9498            json!({
9499                "model": "stub-model",
9500                "messages": [{"role": "user", "content": "Use the weather tool."}],
9501                "stream": true,
9502                "tools": [{
9503                    "type": "function",
9504                    "function": {"name": "weather", "parameters": {"type": "object"}}
9505                }],
9506                "tool_choice": "auto"
9507            }),
9508        )
9509        .await;
9510        assert_eq!(response.status(), AxumStatusCode::OK);
9511        let body = response_text(response).await;
9512        assert!(body.contains("data: [DONE]"), "missing DONE: {body}");
9513        assert!(
9514            body.contains(r#""finish_reason":"tool_calls""#),
9515            "stream should finish with tool_calls: {body}"
9516        );
9517        assert!(
9518            body.contains(r#""tool_calls":[{"index":0,"id":"call_1""#),
9519            "stream should emit tool_calls from chunk api_response: {body}"
9520        );
9521        assert!(
9522            !body.contains("raw text that should not stream"),
9523            "structured api_response should suppress raw generated text in tool-call stream: {body}"
9524        );
9525    }
9526
9527    #[tokio::test]
9528    async fn route_streaming_chat_preserves_length_over_structured_tool_response() {
9529        let generated = r#"{"name":"weather","arguments":{"city":"Paris"}}"#;
9530        let response = post_json(
9531            router_with_stub_api_response_and_finish_reason(
9532                generated,
9533                weather_tool_api_response(),
9534                FinishReason::Length,
9535            ),
9536            "/v1/chat/completions",
9537            json!({
9538                "model": "stub-model",
9539                "messages": [{"role": "user", "content": "Use the weather tool."}],
9540                "stream": true,
9541                "tools": [{
9542                    "type": "function",
9543                    "function": {"name": "weather", "parameters": {"type": "object"}}
9544                }],
9545                "tool_choice": "auto"
9546            }),
9547        )
9548        .await;
9549        assert_eq!(response.status(), AxumStatusCode::OK);
9550        let body = response_text(response).await;
9551        assert!(body.contains("data: [DONE]"), "missing DONE: {body}");
9552        assert!(
9553            body.contains(r#""finish_reason":"length""#),
9554            "stream must preserve the engine terminal reason: {body}"
9555        );
9556        assert!(
9557            !body.contains(r#""finish_reason":"tool_calls""#),
9558            "length must not be relabeled as tool_calls: {body}"
9559        );
9560    }
9561
9562    #[tokio::test]
9563    async fn route_streaming_chat_tool_choice_required_errors_without_leaking_content() {
9564        let response = post_json(
9565            router_with_stub("plain answer"),
9566            "/v1/chat/completions",
9567            json!({
9568                "model": "stub-model",
9569                "messages": [{"role": "user", "content": "Use a tool."}],
9570                "stream": true,
9571                "tools": [{
9572                    "type": "function",
9573                    "function": {"name": "weather", "parameters": {"type": "object"}}
9574                }],
9575                "tool_choice": "required"
9576            }),
9577        )
9578        .await;
9579        assert_eq!(response.status(), AxumStatusCode::OK);
9580        let body = response_text(response).await;
9581        assert_eq!(body.matches("data: [DONE]").count(), 1, "body: {body}");
9582        assert!(
9583            body.contains(
9584                r#""error":{"message":"model output did not satisfy required tool_choice""#
9585            ),
9586            "stream should emit OpenAI error envelope: {body}"
9587        );
9588        assert!(
9589            body.contains(r#""type":"invalid_request_error""#),
9590            "stream should use invalid_request_error: {body}"
9591        );
9592        assert!(
9593            body.contains(r#""param":"tool_choice""#),
9594            "stream should include tool_choice param: {body}"
9595        );
9596        assert!(
9597            !body.contains(r#""content":"plain answer""#),
9598            "required stream must not leak invalid content before validation: {body}"
9599        );
9600    }
9601
9602    #[tokio::test]
9603    async fn route_streaming_chat_tool_request_falls_back_to_content_when_no_tool_call() {
9604        let response = post_json(
9605            router_with_stub("plain answer"),
9606            "/v1/chat/completions",
9607            json!({
9608                "model": "stub-model",
9609                "messages": [{"role": "user", "content": "Use the weather tool if needed."}],
9610                "stream": true,
9611                "tools": [{
9612                    "type": "function",
9613                    "function": {"name": "weather", "parameters": {"type": "object"}}
9614                }],
9615                "tool_choice": "auto"
9616            }),
9617        )
9618        .await;
9619        assert_eq!(response.status(), AxumStatusCode::OK);
9620        let body = response_text(response).await;
9621        assert!(
9622            body.contains(r#""content":"plain answer""#),
9623            "plain content should still stream when no tool call is generated: {body}"
9624        );
9625        assert!(
9626            body.contains(r#""finish_reason":"stop""#),
9627            "plain content should keep normal finish reason: {body}"
9628        );
9629        assert!(
9630            !body.contains(r#""tool_calls""#),
9631            "fallback content should not synthesize tool_calls: {body}"
9632        );
9633    }
9634
9635    #[tokio::test]
9636    async fn route_streaming_chat_serializes_generated_legacy_function_call_delta() {
9637        let response = post_json(
9638            router_with_stub(
9639                r#"{"function_call":{"name":"weather","arguments":{"city":"Paris"}}}"#,
9640            ),
9641            "/v1/chat/completions",
9642            json!({
9643                "model": "stub-model",
9644                "messages": [{"role": "user", "content": "Use the weather function."}],
9645                "stream": true,
9646                "functions": [{
9647                    "name": "weather",
9648                    "parameters": {
9649                        "type": "object",
9650                        "properties": {"city": {"type": "string"}},
9651                        "required": ["city"]
9652                    }
9653                }],
9654                "function_call": "auto"
9655            }),
9656        )
9657        .await;
9658        assert_eq!(response.status(), AxumStatusCode::OK);
9659        let body = response_text(response).await;
9660        assert!(body.contains("data: [DONE]"), "missing DONE: {body}");
9661        assert!(
9662            body.contains(r#""finish_reason":"function_call""#),
9663            "stream should finish with function_call: {body}"
9664        );
9665        assert!(
9666            body.contains(
9667                r#""function_call":{"name":"weather","arguments":"{\"city\":\"Paris\"}"}"#
9668            ),
9669            "stream should emit OpenAI legacy function_call delta: {body}"
9670        );
9671        assert!(
9672            !body.contains(r#""content":"{\"function_call\""#),
9673            "raw function-call JSON should not be streamed as assistant content: {body}"
9674        );
9675    }
9676
9677    #[tokio::test]
9678    async fn route_streaming_chat_honors_specific_legacy_function_call_delta() {
9679        let request = |generated: &'static str| {
9680            post_json(
9681                router_with_stub(generated),
9682                "/v1/chat/completions",
9683                json!({
9684                    "model": "stub-model",
9685                    "messages": [{"role": "user", "content": "Use the selected function."}],
9686                    "stream": true,
9687                    "functions": [
9688                        {"name": "weather", "parameters": {"type": "object"}},
9689                        {"name": "calendar", "parameters": {"type": "object"}}
9690                    ],
9691                    "function_call": {"name": "weather"}
9692                }),
9693            )
9694        };
9695
9696        let response =
9697            request(r#"{"function_call":{"name":"weather","arguments":{"city":"Paris"}}}"#).await;
9698        assert_eq!(response.status(), AxumStatusCode::OK);
9699        let body = response_text(response).await;
9700        assert!(body.contains("data: [DONE]"), "missing DONE: {body}");
9701        assert!(
9702            body.contains(r#""finish_reason":"function_call""#),
9703            "selected function should finish with function_call: {body}"
9704        );
9705        assert!(
9706            body.contains(
9707                r#""function_call":{"name":"weather","arguments":"{\"city\":\"Paris\"}"}"#
9708            ),
9709            "selected function should stream as function_call delta: {body}"
9710        );
9711
9712        let response = request(r#"{"function_call":{"name":"calendar","arguments":{}}}"#).await;
9713        assert_eq!(response.status(), AxumStatusCode::OK);
9714        let body = response_text(response).await;
9715        assert!(
9716            body.contains(
9717                r#""content":"{\"function_call\":{\"name\":\"calendar\",\"arguments\":{}}}""#
9718            ),
9719            "unselected function JSON should stream as ordinary content: {body}"
9720        );
9721        assert!(
9722            body.contains(r#""finish_reason":"stop""#),
9723            "unselected function JSON should keep normal stop finish: {body}"
9724        );
9725        assert!(
9726            !body.contains(r#""finish_reason":"function_call""#),
9727            "unselected function JSON must not become function_call: {body}"
9728        );
9729    }
9730
9731    #[tokio::test]
9732    async fn route_chat_serializes_generated_legacy_function_call_when_engine_returns_text_only() {
9733        let response = post_json(
9734            router_with_stub(
9735                r#"{"function_call":{"name":"weather","arguments":{"city":"Paris"}}}"#,
9736            ),
9737            "/v1/chat/completions",
9738            json!({
9739                "model": "stub-model",
9740                "messages": [{"role": "user", "content": "Use the weather function."}],
9741                "functions": [{
9742                    "name": "weather",
9743                    "parameters": {
9744                        "type": "object",
9745                        "properties": {"city": {"type": "string"}},
9746                        "required": ["city"]
9747                    }
9748                }],
9749                "function_call": "auto"
9750            }),
9751        )
9752        .await;
9753        assert_eq!(response.status(), AxumStatusCode::OK);
9754        let body = response_json(response).await;
9755        assert_eq!(body["choices"][0]["finish_reason"], "function_call");
9756        assert_eq!(body["choices"][0]["message"]["content"], "");
9757        assert_eq!(
9758            body["choices"][0]["message"]["function_call"]["name"],
9759            "weather"
9760        );
9761        assert_eq!(
9762            body["choices"][0]["message"]["function_call"]["arguments"],
9763            "{\"city\":\"Paris\"}"
9764        );
9765    }
9766
9767    #[tokio::test]
9768    async fn route_chat_serializes_legacy_function_call_response() {
9769        let response = post_json(
9770            router_with_stub_api_response(
9771                "",
9772                ferrum_types::ApiResponse::Chat(ferrum_types::ApiChatResponse {
9773                    message: ferrum_types::ApiChatMessage {
9774                        role: ferrum_types::ApiMessageRole::Assistant,
9775                        content: String::new(),
9776                        name: None,
9777                        tool_calls: vec![],
9778                        tool_call_id: None,
9779                        function_call: Some(ferrum_types::ApiFunctionCall {
9780                            name: "weather".to_string(),
9781                            arguments: "{\"city\":\"Paris\"}".to_string(),
9782                        }),
9783                    },
9784                    finish_reason: Some("function_call".to_string()),
9785                }),
9786            ),
9787            "/v1/chat/completions",
9788            json!({
9789                "model": "stub-model",
9790                "messages": [{"role": "user", "content": "Use the weather function."}],
9791                "functions": [{
9792                    "name": "weather",
9793                    "parameters": {
9794                        "type": "object",
9795                        "properties": {"city": {"type": "string"}},
9796                        "required": ["city"]
9797                    }
9798                }],
9799                "function_call": "auto"
9800            }),
9801        )
9802        .await;
9803        assert_eq!(response.status(), AxumStatusCode::OK);
9804        let body = response_json(response).await;
9805        assert_eq!(body["choices"][0]["finish_reason"], "function_call");
9806        assert_eq!(
9807            body["choices"][0]["message"]["function_call"]["name"],
9808            "weather"
9809        );
9810        assert_eq!(
9811            body["choices"][0]["message"]["function_call"]["arguments"],
9812            "{\"city\":\"Paris\"}"
9813        );
9814    }
9815
9816    #[tokio::test]
9817    async fn route_streaming_chat_include_usage_contract() {
9818        let response = post_json(
9819            router_with_stub("ok"),
9820            "/v1/chat/completions",
9821            json!({
9822                "model": "stub-model",
9823                "messages": [{"role": "user", "content": "Say ok"}],
9824                "stream": true,
9825                "stream_options": {"include_usage": true}
9826            }),
9827        )
9828        .await;
9829        assert_eq!(response.status(), AxumStatusCode::OK);
9830        let body = response_text(response).await;
9831        assert!(body.contains("data: [DONE]"), "missing DONE: {body}");
9832        assert!(
9833            body.contains("\"object\":\"chat.completion.chunk\""),
9834            "missing chat chunk: {body}"
9835        );
9836        assert!(
9837            body.contains("\"usage\":{\"prompt_tokens\""),
9838            "missing final usage chunk: {body}"
9839        );
9840        assert!(
9841            body.contains("\"choices\":[],\"usage\""),
9842            "usage should be emitted as a separate chunk: {body}"
9843        );
9844        assert!(
9845            body.contains("\"prompt_tokens\":5"),
9846            "stream usage should come from engine token usage: {body}"
9847        );
9848    }
9849
9850    #[tokio::test]
9851    async fn route_streaming_chat_waits_for_separate_final_usage_at_max_tokens() {
9852        let response = post_json(
9853            router_with_stub_separate_final_stream_chunk(&["he", "llo"]),
9854            "/v1/chat/completions",
9855            json!({
9856                "model": "stub-model",
9857                "messages": [{"role": "user", "content": "Say hello"}],
9858                "max_tokens": 2,
9859                "stream": true,
9860                "stream_options": {"include_usage": true}
9861            }),
9862        )
9863        .await;
9864        assert_eq!(response.status(), AxumStatusCode::OK);
9865        let body = response_text(response).await;
9866        assert_eq!(body.matches("data: [DONE]").count(), 1, "body: {body}");
9867        assert!(
9868            body.contains("\"content\":\"he\""),
9869            "missing first chunk: {body}"
9870        );
9871        assert!(
9872            body.contains("\"content\":\"llo\""),
9873            "missing second chunk: {body}"
9874        );
9875        assert!(
9876            body.contains("\"choices\":[],\"usage\""),
9877            "missing separate usage chunk from final engine chunk: {body}"
9878        );
9879        assert!(
9880            body.contains("\"prompt_tokens\":5"),
9881            "stream usage should come from engine final usage: {body}"
9882        );
9883    }
9884
9885    #[tokio::test]
9886    async fn route_streaming_preserves_tokenless_tail_before_terminal() {
9887        for (path, chunks, expected_content, expected_reasoning) in [
9888            ("/v1/chat/completions", ["hello ", "尾"], "hello 尾", ""),
9889            (
9890                "/v1/chat/completions",
9891                ["<think>reason", "</think>"],
9892                "",
9893                "reason",
9894            ),
9895            ("/v1/completions", ["hello ", "尾"], "hello 尾", ""),
9896        ] {
9897            let chat = path == "/v1/chat/completions";
9898            let mut request = json!({"model": "stub-model", "stream": true});
9899            if chat {
9900                request["messages"] = json!([{"role": "user", "content": "hello"}]);
9901                request["stream_options"] = json!({"include_usage": true});
9902            } else {
9903                request["prompt"] = json!("hello");
9904            }
9905            let router = AxumServer::from_llm(Arc::new(StubLlm::with_tokenless_tail(&chunks)))
9906                .build_router();
9907            let response = post_json(router, path, request).await;
9908            assert_eq!(response.status(), AxumStatusCode::OK);
9909            let body = response_text(response).await;
9910            let events = responses_sse_json_events(&body);
9911            let content: String = events
9912                .iter()
9913                .filter_map(|event| {
9914                    if chat {
9915                        event["choices"][0]["delta"]["content"].as_str()
9916                    } else {
9917                        event["choices"][0]["text"].as_str()
9918                    }
9919                })
9920                .collect();
9921            let reasoning: String = events
9922                .iter()
9923                .filter_map(|event| event["choices"][0]["delta"]["reasoning"].as_str())
9924                .collect();
9925            assert_eq!(content, expected_content, "body: {body}");
9926            assert_eq!(reasoning, expected_reasoning, "body: {body}");
9927            assert_eq!(body.matches("data: [DONE]").count(), 1, "body: {body}");
9928            assert_eq!(
9929                events
9930                    .iter()
9931                    .filter(|event| event["choices"][0]["finish_reason"] == "stop")
9932                    .count(),
9933                1,
9934                "body: {body}"
9935            );
9936            let usage: Vec<_> = events
9937                .iter()
9938                .filter_map(|event| event["usage"].as_object())
9939                .collect();
9940            assert_eq!(usage.len(), 1, "body: {body}");
9941            assert_eq!(usage[0]["prompt_tokens"], 5);
9942            assert_eq!(usage[0]["completion_tokens"], 2);
9943        }
9944    }
9945
9946    #[tokio::test]
9947    async fn route_rejects_multimodal_content_with_400() {
9948        for content in [
9949            json!([{"type": "image_url", "image_url": {"url": "https://example.com/image.png"}}]),
9950            json!([{"type": "image_url", "image_url": {"url": "data:image/png;base64,AA=="}}]),
9951            json!([{"type": "video_url", "video_url": {"url": "https://example.com/video.mp4"}}]),
9952            json!([
9953                {"type": "text", "text": "describe this"},
9954                {"type": "image_url", "image_url": {"url": "https://example.com/image.png"}}
9955            ]),
9956        ] {
9957            let response = post_json(
9958                router_with_stub("unused"),
9959                "/v1/chat/completions",
9960                json!({
9961                    "model": "stub-model",
9962                    "messages": [{"role": "user", "content": content}]
9963                }),
9964            )
9965            .await;
9966            assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
9967            let body = response_json(response).await;
9968            assert_eq!(body["error"]["type"], "invalid_request_error");
9969            let message = body["error"]["message"].as_str().unwrap();
9970            assert!(message.contains("invalid chat completions request"));
9971            assert!(
9972                message.contains("unsupported message content part type"),
9973                "body: {body}"
9974            );
9975        }
9976    }
9977
9978    #[tokio::test]
9979    async fn route_rejects_non_object_stream_options() {
9980        for stream_options in [json!([]), json!("yes"), json!(42), json!(true)] {
9981            let response = post_json(
9982                router_with_stub("unused"),
9983                "/v1/chat/completions",
9984                json!({
9985                    "model": "stub-model",
9986                    "messages": [{"role": "user", "content": "hello"}],
9987                    "stream": true,
9988                    "stream_options": stream_options
9989                }),
9990            )
9991            .await;
9992            assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
9993            let body = response_json(response).await;
9994            assert_eq!(body["error"]["type"], "invalid_request_error");
9995            assert!(
9996                body["error"]["message"]
9997                    .as_str()
9998                    .unwrap_or_default()
9999                    .contains("stream_options must be a JSON object"),
10000                "body: {body}"
10001            );
10002        }
10003    }
10004
10005    #[tokio::test]
10006    async fn route_accepts_text_only_content_array() {
10007        let response = post_json(
10008            router_with_stub("ok"),
10009            "/v1/chat/completions",
10010            json!({
10011                "model": "stub-model",
10012                "messages": [{
10013                    "role": "user",
10014                    "content": [
10015                        {"type": "text", "text": "say"},
10016                        {"type": "text", "text": "ok"}
10017                    ]
10018                }]
10019            }),
10020        )
10021        .await;
10022        assert_eq!(response.status(), AxumStatusCode::OK);
10023        let body = response_json(response).await;
10024        assert_eq!(body["choices"][0]["message"]["content"], "ok");
10025    }
10026
10027    #[tokio::test]
10028    async fn route_chat_invalid_json_maps_to_openai_error() {
10029        let response = post_raw_json(router_with_stub("unused"), "/v1/chat/completions", "{").await;
10030        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
10031        let body = response_json(response).await;
10032        assert_eq!(body["error"]["type"], "invalid_request_error");
10033        assert_eq!(body["error"]["param"], Value::Null);
10034        assert!(body["error"]["message"]
10035            .as_str()
10036            .unwrap()
10037            .contains("invalid chat completions request"));
10038    }
10039
10040    #[tokio::test]
10041    async fn route_rejects_logit_bias_with_openai_error_param() {
10042        let response = post_json(
10043            router_with_stub("unused"),
10044            "/v1/chat/completions",
10045            json!({
10046                "model": "stub-model",
10047                "messages": [{"role": "user", "content": "hello"}],
10048                "logit_bias": {"1": 42.0}
10049            }),
10050        )
10051        .await;
10052        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
10053        let body = response_json(response).await;
10054        assert_eq!(body["error"]["type"], "invalid_request_error");
10055        assert_eq!(body["error"]["param"], "logit_bias");
10056    }
10057
10058    #[tokio::test]
10059    async fn route_tool_request_reaches_engine_structured_boundary() {
10060        for stream in [false, true] {
10061            let (router, engine) = router_with_capturing_llm();
10062            let response = post_json(
10063                router,
10064                "/v1/chat/completions",
10065                json!({
10066                    "model": "qwen3",
10067                    "messages": [
10068                        {"role": "user", "content": "Use the weather tool."},
10069                        {
10070                            "role": "assistant",
10071                            "content": null,
10072                            "tool_calls": [{
10073                                "id": "call_1",
10074                                "type": "function",
10075                                "function": {"name": "weather", "arguments": "{\"city\":\"Paris\"}"}
10076                            }]
10077                        },
10078                        {"role": "tool", "tool_call_id": "call_1", "content": "sunny"}
10079                    ],
10080                    "tools": [{
10081                        "type": "function",
10082                        "function": {
10083                            "name": "weather",
10084                            "description": "Get weather",
10085                            "parameters": {
10086                                "type": "object",
10087                                "properties": {"city": {"type": "string"}},
10088                                "required": ["city"]
10089                            }
10090                        }
10091                    }],
10092                    "tool_choice": "auto",
10093                    "functions": [{
10094                        "name": "legacy_weather",
10095                        "parameters": {"type": "object", "properties": {}}
10096                    }],
10097                    "function_call": "auto",
10098                    "stream": stream
10099                }),
10100            )
10101            .await;
10102            assert_eq!(response.status(), AxumStatusCode::OK);
10103
10104            if stream {
10105                let body = response_text(response).await;
10106                assert!(body.contains("[DONE]"), "{body}");
10107                assert!(body.contains("captured"), "{body}");
10108            } else {
10109                let body = response_json(response).await;
10110                assert_eq!(body["choices"][0]["message"]["content"], "captured");
10111                assert_eq!(body["choices"][0]["finish_reason"], "stop");
10112            }
10113            let request = engine.last_request();
10114            assert!(request.prompt.contains("\"tools\":[{"));
10115            assert!(request.prompt.contains("\"type\":\"function\""));
10116            assert!(request.prompt.contains("\"name\":\"weather\""));
10117            assert!(request.prompt.contains("<|im_start|>assistant\n{"));
10118            assert!(request.prompt.contains("\"tool_calls\":[{"));
10119            assert!(request.prompt.contains("\"id\":\"call_1\""));
10120            assert!(request.prompt.contains("<|im_start|>tool\nsunny<|im_end|>"));
10121            assert_eq!(
10122                request.metadata["openai_tools"][0]["function"]["name"],
10123                "weather"
10124            );
10125            assert_eq!(request.metadata["openai_tool_choice"], "auto");
10126            assert_eq!(
10127                request.metadata["openai_legacy_functions"][0]["name"],
10128                "legacy_weather"
10129            );
10130            assert_eq!(request.metadata["openai_legacy_function_call"], "auto");
10131            let Some(ferrum_types::ApiRequest::Chat(api)) = request.api_request.as_ref() else {
10132                panic!("expected structured chat api_request");
10133            };
10134            assert_eq!(api.messages.len(), 3);
10135            assert_eq!(api.messages[2].role, ferrum_types::ApiMessageRole::Tool);
10136            assert_eq!(api.messages[2].tool_call_id.as_deref(), Some("call_1"));
10137            assert_eq!(api.messages[1].tool_calls[0].id, "call_1");
10138            assert_eq!(api.messages[1].tool_calls[0].function.name, "weather");
10139            assert_eq!(api.messages[2].content, "sunny");
10140            assert_eq!(api.tools[0].function.name, "weather");
10141            assert_eq!(api.legacy_functions[0].name, "legacy_weather");
10142            assert_eq!(
10143                api.messages[1].tool_calls[0].function.arguments,
10144                "{\"city\":\"Paris\"}"
10145            );
10146        }
10147    }
10148
10149    #[tokio::test]
10150    async fn route_replays_reasoning_content_in_qwen36_tool_history_sync() {
10151        let compatibility = capture_qwen36_tool_history_request(
10152            json!({"reasoning_content": "opencode-reasoning-marker"}),
10153            false,
10154        )
10155        .await;
10156        let canonical = capture_qwen36_tool_history_request(
10157            json!({"reasoning": "opencode-reasoning-marker"}),
10158            false,
10159        )
10160        .await;
10161
10162        assert_eq!(compatibility.prompt, canonical.prompt);
10163        assert!(
10164            compatibility.prompt.contains("opencode-reasoning-marker"),
10165            "Qwen3.6 prompt dropped assistant reasoning history: {}",
10166            compatibility.prompt
10167        );
10168        let message = &compatibility.metadata["openai_messages"][1];
10169        assert_eq!(message["reasoning"], "opencode-reasoning-marker");
10170        assert!(message.get("reasoning_content").is_none());
10171    }
10172
10173    #[tokio::test]
10174    async fn route_replays_reasoning_content_in_qwen36_tool_history_stream() {
10175        let request = capture_qwen36_tool_history_request(
10176            json!({"reasoning_content": "opencode-stream-reasoning-marker"}),
10177            true,
10178        )
10179        .await;
10180        assert!(
10181            request.prompt.contains("opencode-stream-reasoning-marker"),
10182            "Qwen3.6 streaming prompt dropped assistant reasoning history: {}",
10183            request.prompt
10184        );
10185    }
10186
10187    #[tokio::test]
10188    async fn route_prefers_canonical_reasoning_in_qwen36_tool_history() {
10189        for stream in [false, true] {
10190            for reasoning in ["canonical-history-marker", ""] {
10191                let request = capture_qwen36_tool_history_request(
10192                    json!({
10193                        "reasoning": reasoning,
10194                        "reasoning_content": "alias-history-marker"
10195                    }),
10196                    stream,
10197                )
10198                .await;
10199                let canonical =
10200                    capture_qwen36_tool_history_request(json!({"reasoning": reasoning}), stream)
10201                        .await;
10202                assert_eq!(request.prompt, canonical.prompt);
10203                assert!(!request.prompt.contains("alias-history-marker"));
10204                let message = &request.metadata["openai_messages"][1];
10205                assert_eq!(message["reasoning"], reasoning);
10206                assert!(message.get("reasoning_content").is_none());
10207            }
10208        }
10209    }
10210
10211    #[tokio::test]
10212    async fn route_does_not_force_reasoning_into_templates_that_ignore_it() {
10213        let template = ModelChatTemplate::new(
10214            "{% for message in messages %}[{{ message.role }}]{{ message.content }}{% endfor %}",
10215            "content-only-template",
10216        );
10217        let (router, engine) = router_with_capturing_llm_and_template(template);
10218        let response = post_json(
10219            router,
10220            "/v1/chat/completions",
10221            json!({
10222                "model": "served-alias",
10223                "messages": [
10224                    {"role": "user", "content": "hello"},
10225                    {
10226                        "role": "assistant",
10227                        "content": "visible answer",
10228                        "reasoning_content": "hidden-reasoning-marker"
10229                    },
10230                    {"role": "user", "content": "continue"}
10231                ]
10232            }),
10233        )
10234        .await;
10235        assert_eq!(response.status(), AxumStatusCode::OK);
10236
10237        let request = engine.last_request();
10238        assert!(request.prompt.contains("visible answer"));
10239        assert!(!request.prompt.contains("hidden-reasoning-marker"));
10240    }
10241
10242    #[tokio::test]
10243    async fn route_tool_request_prefers_model_chat_template() {
10244        for stream in [false, true] {
10245            let template = ModelChatTemplate::new(
10246            "{% if tools %}<tools>{% for tool in tools %}{{ tool.function.name }}{% endfor %}</tools>{% endif %}{% for message in messages %}[{{ message.role }}]{{ message.content }}{% if message.tool_calls %}{% for tool_call in message.tool_calls %}<tool_call id=\"{{ tool_call.id }}\">{{ tool_call.function.name }}:{{ tool_call.function.arguments }}</tool_call>{% endfor %}{% endif %}{% if message.tool_call_id %}<tool_response id=\"{{ message.tool_call_id }}\">{{ message.content }}</tool_response>{% endif %}{% endfor %}{% if add_generation_prompt %}[assistant]{% endif %}",
10247            "tool-template",
10248        );
10249            let (router, engine) = router_with_capturing_llm_and_template(template);
10250            let response = post_json(
10251            router,
10252            "/v1/chat/completions",
10253            json!({
10254                "model": "served-alias",
10255                "messages": [
10256                    {"role": "user", "content": "Use the weather tool."},
10257                    {
10258                        "role": "assistant",
10259                        "content": null,
10260                        "tool_calls": [{
10261                            "id": "weather_paris",
10262                            "type": "function",
10263                            "function": {"name": "weather", "arguments": "{\"city\":\"Paris\"}"}
10264                        }, {
10265                            "id": "weather_rome",
10266                            "type": "function",
10267                            "function": {"name": "weather", "arguments": "{\"city\":\"Rome\"}"}
10268                        }]
10269                    },
10270                    // Results may arrive in a different order than calls. IDs,
10271                    // rather than positions or function names, preserve pairing.
10272                    {"role": "tool", "tool_call_id": "weather_rome", "content": "rainy"},
10273                    {"role": "tool", "tool_call_id": "weather_paris", "content": "sunny"}
10274                ],
10275                "tools": [{
10276                    "type": "function",
10277                    "function": {
10278                        "name": "weather",
10279                        "description": "Get weather",
10280                        "parameters": {"type": "object", "properties": {"city": {"type": "string"}}}
10281                    }
10282                }],
10283                "tool_choice": "auto",
10284                "stream": stream
10285            }),
10286        )
10287        .await;
10288            assert_eq!(response.status(), AxumStatusCode::OK);
10289
10290            if stream {
10291                let body = response_text(response).await;
10292                assert!(body.contains("[DONE]"), "{body}");
10293                assert!(body.contains("captured"), "{body}");
10294            } else {
10295                let body = response_json(response).await;
10296                assert_eq!(body["choices"][0]["message"]["content"], "captured");
10297                assert_eq!(body["choices"][0]["finish_reason"], "stop");
10298            }
10299            let request = engine.last_request();
10300            assert!(request.prompt.contains("<tools>weather</tools>"));
10301            assert!(
10302                request
10303                    .prompt
10304                    .contains("<tool_call id=\"weather_paris\">weather:"),
10305                "{}",
10306                request.prompt
10307            );
10308            assert!(request.prompt.contains("\"city\""), "{}", request.prompt);
10309            assert!(request.prompt.contains("Paris"), "{}", request.prompt);
10310            assert!(request
10311                .prompt
10312                .contains("<tool_response id=\"weather_paris\">sunny</tool_response>"));
10313            assert!(request
10314                .prompt
10315                .contains("<tool_call id=\"weather_rome\">weather:"));
10316            assert!(request.prompt.contains("Rome"));
10317            assert!(request
10318                .prompt
10319                .contains("<tool_response id=\"weather_rome\">rainy</tool_response>"));
10320            assert!(request.prompt.ends_with("[assistant]"));
10321            let Some(ferrum_types::ApiRequest::Chat(api)) = request.api_request.as_ref() else {
10322                panic!("expected structured continuation request");
10323            };
10324            assert_eq!(api.messages.len(), 4);
10325            assert_eq!(api.messages[1].tool_calls.len(), 2);
10326            for (call, id, city) in [
10327                (&api.messages[1].tool_calls[0], "weather_paris", "Paris"),
10328                (&api.messages[1].tool_calls[1], "weather_rome", "Rome"),
10329            ] {
10330                assert_eq!(call.id, id);
10331                assert_eq!(call.function.name, "weather");
10332                let args: Value = serde_json::from_str(&call.function.arguments).unwrap();
10333                assert_eq!(args, json!({"city": city}));
10334                let prefix = format!("<tool_call id=\"{id}\">weather:");
10335                let rendered_arguments = request
10336                    .prompt
10337                    .split_once(&prefix)
10338                    .unwrap()
10339                    .1
10340                    .split_once("</tool_call>")
10341                    .unwrap()
10342                    .0;
10343                let rendered: Value = serde_json::from_str(rendered_arguments).unwrap();
10344                assert_eq!(
10345                    rendered,
10346                    json!({"city": city}),
10347                    "tool arguments lost their call ID binding"
10348                );
10349            }
10350            for (message, id, content) in [
10351                (&api.messages[2], "weather_rome", "rainy"),
10352                (&api.messages[3], "weather_paris", "sunny"),
10353            ] {
10354                assert_eq!(message.role, ferrum_types::ApiMessageRole::Tool);
10355                assert_eq!(message.tool_call_id.as_deref(), Some(id));
10356                assert_eq!(message.content, content);
10357            }
10358            assert!(
10359                !request.prompt.contains("<|assistant|>"),
10360                "model-template tool prompt should not use generic fallback: {}",
10361                request.prompt
10362            );
10363            assert!(
10364                !request.prompt.contains("When a tool is needed"),
10365                "model-template tool prompt should not inject fallback tool instructions: {}",
10366                request.prompt
10367            );
10368        }
10369    }
10370
10371    #[tokio::test]
10372    async fn chat_omitted_output_budget_uses_auto_ceiling() {
10373        let (router, engine) = router_with_capturing_llm();
10374        let response = post_json(
10375            router,
10376            "/v1/chat/completions",
10377            json!({
10378                "model": "stub-model",
10379                "messages": [{"role": "user", "content": "hello"}]
10380            }),
10381        )
10382        .await;
10383        assert_eq!(response.status(), AxumStatusCode::OK);
10384
10385        let request = engine.last_request();
10386        assert_eq!(request.sampling_params.max_tokens, 4096);
10387        assert_eq!(
10388            request.metadata.get(DEFAULT_MAX_TOKENS_METADATA_KEY),
10389            Some(&serde_json::json!(true))
10390        );
10391    }
10392
10393    #[tokio::test]
10394    async fn chat_accepts_stop_string_and_max_completion_tokens() {
10395        let (router, engine) = router_with_capturing_llm();
10396        let response = post_json(
10397            router,
10398            "/v1/chat/completions",
10399            json!({
10400                "model": "stub-model",
10401                "messages": [{"role": "user", "content": "hello"}],
10402                "max_tokens": 99,
10403                "max_completion_tokens": 3,
10404                "stop": "<END>"
10405            }),
10406        )
10407        .await;
10408        assert_eq!(response.status(), AxumStatusCode::OK);
10409
10410        let request = engine.last_request();
10411        let defaults = default_chat_sampling_params();
10412        assert_eq!(request.sampling_params.max_tokens, 3);
10413        assert!(!request
10414            .metadata
10415            .contains_key(DEFAULT_MAX_TOKENS_METADATA_KEY));
10416        assert_eq!(request.sampling_params.temperature, defaults.temperature);
10417        assert_eq!(
10418            request.sampling_params.repetition_penalty,
10419            defaults.repetition_penalty
10420        );
10421        assert_eq!(request.sampling_params.stop_sequences, vec!["<END>"]);
10422    }
10423
10424    #[tokio::test]
10425    async fn chat_maps_vllm_sampling_extensions_without_hidden_defaults() {
10426        let (router, engine) = router_with_capturing_llm();
10427        let response = post_json(
10428            router,
10429            "/v1/chat/completions",
10430            json!({
10431                "model": "stub-model",
10432                "messages": [{"role": "user", "content": "hello"}],
10433                "top_k": 20,
10434                "min_p": 0.05,
10435                "repetition_penalty": 1.25
10436            }),
10437        )
10438        .await;
10439        assert_eq!(response.status(), AxumStatusCode::OK);
10440
10441        let request = engine.last_request();
10442        assert_eq!(request.sampling_params.top_k, Some(20));
10443        assert_eq!(request.sampling_params.min_p, Some(0.05));
10444        assert_eq!(request.sampling_params.repetition_penalty, 1.25);
10445    }
10446
10447    #[tokio::test]
10448    async fn chat_normalizes_disabled_sampling_extensions_and_rejects_invalid_ranges() {
10449        let (router, engine) = router_with_capturing_llm();
10450        let response = post_json(
10451            router,
10452            "/v1/chat/completions",
10453            json!({
10454                "model": "stub-model",
10455                "messages": [{"role": "user", "content": "hello"}],
10456                "top_k": -1,
10457                "min_p": 0.0,
10458                "repetition_penalty": 1.0
10459            }),
10460        )
10461        .await;
10462        assert_eq!(response.status(), AxumStatusCode::OK);
10463        let request = engine.last_request();
10464        assert_eq!(request.sampling_params.top_k, None);
10465        assert_eq!(request.sampling_params.min_p, None);
10466        assert_eq!(request.sampling_params.repetition_penalty, 1.0);
10467
10468        for (field, value) in [
10469            ("top_k", json!(-2)),
10470            ("min_p", json!(1.01)),
10471            ("repetition_penalty", json!(0.0)),
10472            ("presence_penalty", json!(2.01)),
10473            ("frequency_penalty", json!(-2.01)),
10474        ] {
10475            let (router, _) = router_with_capturing_llm();
10476            let response = post_json(
10477                router,
10478                "/v1/chat/completions",
10479                json!({
10480                    "model": "stub-model",
10481                    "messages": [{"role": "user", "content": "hello"}],
10482                    (field): value
10483                }),
10484            )
10485            .await;
10486            assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST, "{field}");
10487            let body = response_json(response).await;
10488            assert_eq!(body["error"]["param"], field);
10489        }
10490    }
10491
10492    #[tokio::test]
10493    async fn chat_request_forbids_initial_think_close_token() {
10494        let engine = Arc::new(CapturingLlm::new());
10495        let router = AxumServer::from_llm(engine.clone()).build_router();
10496        let response = post_json(
10497            router,
10498            "/v1/chat/completions",
10499            json!({
10500                "model": "qwen3",
10501                "messages": [{"role": "user", "content": "hello"}]
10502            }),
10503        )
10504        .await;
10505        assert_eq!(response.status(), AxumStatusCode::OK);
10506
10507        let request = engine.last_request();
10508        assert_eq!(
10509            request
10510                .metadata
10511                .get(INITIAL_FORBIDDEN_TOKEN_TEXTS_METADATA_KEY),
10512            Some(&serde_json::json!([THINK_END_TAG]))
10513        );
10514    }
10515
10516    #[tokio::test]
10517    async fn omitted_enable_thinking_preserves_model_template_default() {
10518        let template = ModelChatTemplate::new(
10519            "{% for message in messages %}{{ '<|im_start|>' ~ message.role ~ '\n' ~ message.content ~ '<|im_end|>\n' }}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant\n' }}{% if enable_thinking is defined and enable_thinking is false %}{{ '<think>\n\n</think>\n\n' }}{% else %}{{ '<think>\n' }}{% endif %}{% endif %}",
10520            "test-template",
10521        );
10522        let (router, engine) = router_with_capturing_llm_and_template(template);
10523        let response = post_json(
10524            router,
10525            "/v1/chat/completions",
10526            json!({
10527                "model": "served-alias",
10528                "messages": [{"role": "user", "content": "hello"}]
10529            }),
10530        )
10531        .await;
10532        assert_eq!(response.status(), AxumStatusCode::OK);
10533
10534        let request = engine.last_request();
10535        assert!(request.prompt.ends_with("<|im_start|>assistant\n<think>\n"));
10536        assert!(!request
10537            .metadata
10538            .contains_key(INITIAL_FORBIDDEN_TOKEN_TEXTS_METADATA_KEY));
10539    }
10540
10541    #[tokio::test]
10542    async fn server_thinking_default_applies_but_request_override_wins() {
10543        let template = ModelChatTemplate::new(
10544            "{% if add_generation_prompt %}<assistant>{% if enable_thinking is defined and enable_thinking is false %}<think>\n\n</think>\n\n{% else %}<think>\n{% endif %}{% endif %}",
10545            "test-template",
10546        );
10547        let (router, engine) =
10548            router_with_capturing_llm_and_template_default(template, Some(false));
10549
10550        let response = post_json(
10551            router.clone(),
10552            "/v1/chat/completions",
10553            json!({
10554                "model": "served-alias",
10555                "messages": [{"role": "user", "content": "hello"}]
10556            }),
10557        )
10558        .await;
10559        assert_eq!(response.status(), AxumStatusCode::OK);
10560        let request = engine.last_request();
10561        assert_eq!(request.prompt, "<assistant><think>\n\n</think>\n\n");
10562        assert_eq!(
10563            request
10564                .metadata
10565                .get(INITIAL_FORBIDDEN_TOKEN_TEXTS_METADATA_KEY),
10566            Some(&serde_json::json!([THINK_END_TAG, THINK_START_TAG]))
10567        );
10568
10569        let response = post_json(
10570            router,
10571            "/v1/chat/completions",
10572            json!({
10573                "model": "served-alias",
10574                "messages": [{"role": "user", "content": "hello"}],
10575                "chat_template_kwargs": {"enable_thinking": true}
10576            }),
10577        )
10578        .await;
10579        assert_eq!(response.status(), AxumStatusCode::OK);
10580        let request = engine.last_request();
10581        assert_eq!(request.prompt, "<assistant><think>\n");
10582        assert!(!request
10583            .metadata
10584            .contains_key(INITIAL_FORBIDDEN_TOKEN_TEXTS_METADATA_KEY));
10585    }
10586
10587    #[tokio::test]
10588    async fn chat_template_enable_thinking_true_overrides_default() {
10589        let template = ModelChatTemplate::new(
10590            "{% if add_generation_prompt %}<assistant>{% if enable_thinking is defined and enable_thinking is false %}<think>\n\n</think>\n\n{% else %}<think>\n{% endif %}{% endif %}",
10591            "test-template",
10592        );
10593        let (router, engine) = router_with_capturing_llm_and_template(template);
10594        let response = post_json(
10595            router,
10596            "/v1/chat/completions",
10597            json!({
10598                "model": "served-alias",
10599                "messages": [{"role": "user", "content": "hello"}],
10600                "chat_template_kwargs": {"enable_thinking": true}
10601            }),
10602        )
10603        .await;
10604        assert_eq!(response.status(), AxumStatusCode::OK);
10605
10606        let request = engine.last_request();
10607        assert_eq!(request.prompt, "<assistant><think>\n");
10608        assert!(!request
10609            .metadata
10610            .contains_key(INITIAL_FORBIDDEN_TOKEN_TEXTS_METADATA_KEY));
10611    }
10612
10613    #[tokio::test]
10614    async fn chat_template_enable_thinking_false_is_a_hard_override() {
10615        let template = ModelChatTemplate::new(
10616            "{% if add_generation_prompt %}<assistant>{% if enable_thinking is defined and enable_thinking is false %}<think>\n\n</think>\n\n{% else %}<think>\n{% endif %}{% endif %}",
10617            "test-template",
10618        );
10619        let (router, engine) = router_with_capturing_llm_and_template(template);
10620        let response = post_json(
10621            router,
10622            "/v1/chat/completions",
10623            json!({
10624                "model": "served-alias",
10625                "messages": [{"role": "user", "content": "hello"}],
10626                "chat_template_kwargs": {"enable_thinking": false}
10627            }),
10628        )
10629        .await;
10630        assert_eq!(response.status(), AxumStatusCode::OK);
10631
10632        let request = engine.last_request();
10633        assert_eq!(request.prompt, "<assistant><think>\n\n</think>\n\n");
10634        assert_eq!(
10635            request
10636                .metadata
10637                .get(INITIAL_FORBIDDEN_TOKEN_TEXTS_METADATA_KEY),
10638            Some(&serde_json::json!([THINK_END_TAG, THINK_START_TAG]))
10639        );
10640    }
10641
10642    #[tokio::test]
10643    async fn chat_template_reasoning_effort_is_typed_and_rendered() {
10644        let template = ModelChatTemplate::new(
10645            "{% if reasoning_effort is defined %}Reasoning: {{ reasoning_effort }}{% else %}Reasoning: model-default{% endif %}",
10646            "test-template",
10647        );
10648        let (router, engine) = router_with_capturing_llm_and_template(template);
10649        let response = post_json(
10650            router.clone(),
10651            "/v1/chat/completions",
10652            json!({
10653                "model": "served-alias",
10654                "messages": [{"role": "user", "content": "hello"}],
10655                "chat_template_kwargs": {"reasoning_effort": "low"}
10656            }),
10657        )
10658        .await;
10659        assert_eq!(response.status(), AxumStatusCode::OK);
10660        assert_eq!(engine.last_request().prompt, "Reasoning: low");
10661
10662        let response = post_json(
10663            router.clone(),
10664            "/v1/chat/completions",
10665            json!({
10666                "model": "served-alias",
10667                "messages": [{"role": "user", "content": "hello"}],
10668                "chat_template_kwargs": {"reasoning_effort": "xhigh"}
10669            }),
10670        )
10671        .await;
10672        assert_eq!(response.status(), AxumStatusCode::OK);
10673        assert_eq!(engine.last_request().prompt, "Reasoning: xhigh");
10674
10675        for invalid in [json!("extreme"), json!(1)] {
10676            let response = post_json(
10677                router.clone(),
10678                "/v1/chat/completions",
10679                json!({
10680                    "model": "served-alias",
10681                    "messages": [{"role": "user", "content": "hello"}],
10682                    "chat_template_kwargs": {"reasoning_effort": invalid}
10683                }),
10684            )
10685            .await;
10686            assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
10687            let body = response_json(response).await;
10688            assert_eq!(body["error"]["type"], "invalid_request_error");
10689            assert!(body["error"]["message"]
10690                .as_str()
10691                .unwrap_or_default()
10692                .contains("reasoning_effort"));
10693        }
10694    }
10695
10696    #[tokio::test]
10697    async fn chat_template_enable_thinking_rejects_non_bool() {
10698        let template = ModelChatTemplate::new(
10699            "{% if add_generation_prompt %}<assistant>{% endif %}",
10700            "test-template",
10701        );
10702        let (router, _) = router_with_capturing_llm_and_template(template);
10703        let response = post_json(
10704            router,
10705            "/v1/chat/completions",
10706            json!({
10707                "model": "served-alias",
10708                "messages": [{"role": "user", "content": "hello"}],
10709                "chat_template_kwargs": {"enable_thinking": "false"}
10710            }),
10711        )
10712        .await;
10713        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
10714        let body = response_json(response).await;
10715        assert_eq!(body["error"]["type"], "invalid_request_error");
10716        assert!(body["error"]["message"]
10717            .as_str()
10718            .unwrap_or_default()
10719            .contains("chat_template_kwargs.enable_thinking must be a boolean"));
10720    }
10721
10722    #[tokio::test]
10723    async fn stop_string_strips_chat_and_completion_suffixes() {
10724        let chat = post_json(
10725            router_with_stub("hello<END>"),
10726            "/v1/chat/completions",
10727            json!({
10728                "model": "stub-model",
10729                "messages": [{"role": "user", "content": "hello"}],
10730                "stop": "<END>"
10731            }),
10732        )
10733        .await;
10734        assert_eq!(chat.status(), AxumStatusCode::OK);
10735        let chat_body = response_json(chat).await;
10736        assert_eq!(chat_body["choices"][0]["message"]["content"], "hello");
10737
10738        let completion = post_json(
10739            router_with_stub("done<END>"),
10740            "/v1/completions",
10741            json!({
10742                "model": "stub-model",
10743                "prompt": "complete",
10744                "stop": "<END>"
10745            }),
10746        )
10747        .await;
10748        assert_eq!(completion.status(), AxumStatusCode::OK);
10749        let completion_body = response_json(completion).await;
10750        assert_eq!(completion_body["choices"][0]["text"], "done");
10751    }
10752
10753    #[test]
10754    fn started_in_think_parse_streams_reasoning_before_end_tag() {
10755        // R1-distill templates open `<think>` inside the prompt, so the
10756        // generated text never contains the start tag. Mid-think text must
10757        // be reasoning, not content (this leaked as content deltas before).
10758        let parsed = parse_reasoning_response_started_in_think("Okay, the user wants");
10759        assert_eq!(parsed.reasoning.as_deref(), Some("Okay, the user wants"));
10760        assert_eq!(parsed.content, "");
10761
10762        let parsed = parse_reasoning_response_started_in_think("thinking...</think>\nanswer");
10763        assert_eq!(parsed.reasoning.as_deref(), Some("thinking..."));
10764        assert_eq!(parsed.content, "answer");
10765
10766        // Model re-opening its own think block defers to the normal parse.
10767        let parsed = parse_reasoning_response_started_in_think("<think>\nx\n</think>\n\nanswer");
10768        assert_eq!(parsed.reasoning.as_deref(), Some("\nx\n"));
10769        assert_eq!(parsed.content, "answer");
10770    }
10771
10772    #[tokio::test]
10773    async fn chat_response_splits_reasoning_from_content() {
10774        let response = post_json(
10775            router_with_stub("<think>\nreasoning\n</think>\n\nfinal answer"),
10776            "/v1/chat/completions",
10777            json!({
10778                "model": "stub-model",
10779                "messages": [{"role": "user", "content": "hello"}]
10780            }),
10781        )
10782        .await;
10783        assert_eq!(response.status(), AxumStatusCode::OK);
10784
10785        let body = response_json(response).await;
10786        let message = &body["choices"][0]["message"];
10787        assert_eq!(message["content"], "final answer");
10788        assert_eq!(message["reasoning"], "\nreasoning\n");
10789        assert!(message.get("reasoning_content").is_none());
10790    }
10791
10792    #[tokio::test]
10793    async fn streaming_chat_reasoning_prefix_chunks_do_not_panic_or_leak_content() {
10794        let response = post_json(
10795            router_with_stub_stream_chunks(&["<", "think", ">\nreason", "\n</think>\n\nfinal"]),
10796            "/v1/chat/completions",
10797            json!({
10798                "model": "stub-model",
10799                "messages": [{"role": "user", "content": "think then answer"}],
10800                "stream": true
10801            }),
10802        )
10803        .await;
10804        assert_eq!(response.status(), AxumStatusCode::OK);
10805        let body = response_text(response).await;
10806        assert!(body.contains("data: [DONE]"), "missing DONE: {body}");
10807        assert!(
10808            body.contains(r#""reasoning":"\nreason"#),
10809            "stream should emit reasoning delta after full think prefix: {body}"
10810        );
10811        assert!(!body.contains("\"reasoning_content\":"));
10812        assert!(
10813            body.contains(r#""content":"final""#),
10814            "stream should emit visible content after think close: {body}"
10815        );
10816        assert!(
10817            !body.contains(r#""content":"<"#),
10818            "partial think prefix must not leak as content: {body}"
10819        );
10820    }
10821
10822    #[tokio::test]
10823    async fn route_rejects_unsupported_tool_and_function_selection() {
10824        for (extra, param) in [
10825            (
10826                json!({
10827                    "tools": [{
10828                        "type": "function",
10829                        "function": {"name": "weather", "parameters": {"type": "object"}}
10830                    }],
10831                    "tool_choice": {
10832                        "type": "function",
10833                        "function": {"name": "calendar"}
10834                    }
10835                }),
10836                "tool_choice",
10837            ),
10838            (
10839                json!({
10840                    "functions": [{"name": "weather", "parameters": {"type": "object"}}],
10841                    "function_call": {"name": "calendar"}
10842                }),
10843                "function_call",
10844            ),
10845        ] {
10846            let mut body = json!({
10847                "model": "stub-model",
10848                "messages": [{"role": "user", "content": "hello"}]
10849            });
10850            body.as_object_mut()
10851                .expect("object")
10852                .extend(extra.as_object().expect("extra object").clone());
10853            let response =
10854                post_json(router_with_stub("unused"), "/v1/chat/completions", body).await;
10855            assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
10856            let body = response_json(response).await;
10857            assert_eq!(body["error"]["type"], "invalid_request_error");
10858            assert_eq!(body["error"]["param"], param);
10859        }
10860    }
10861
10862    #[tokio::test]
10863    async fn route_rejects_non_function_tools_with_openai_error_param() {
10864        let response = post_json(
10865            router_with_stub("unused"),
10866            "/v1/chat/completions",
10867            json!({
10868                "model": "stub-model",
10869                "messages": [{"role": "user", "content": "hello"}],
10870                "tools": [{
10871                    "type": "retrieval",
10872                    "function": {"name": "search", "parameters": {"type": "object"}}
10873                }]
10874            }),
10875        )
10876        .await;
10877        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
10878        let body = response_json(response).await;
10879        assert_eq!(body["error"]["type"], "invalid_request_error");
10880        assert_eq!(body["error"]["param"], "tools");
10881    }
10882
10883    #[tokio::test]
10884    async fn route_rejects_tool_choice_required_without_tools() {
10885        let response = post_json(
10886            router_with_stub("unused"),
10887            "/v1/chat/completions",
10888            json!({
10889                "model": "stub-model",
10890                "messages": [{"role": "user", "content": "hello"}],
10891                "tool_choice": "required"
10892            }),
10893        )
10894        .await;
10895        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
10896        let body = response_json(response).await;
10897        assert_eq!(body["error"]["type"], "invalid_request_error");
10898        assert_eq!(body["error"]["param"], "tool_choice");
10899    }
10900
10901    #[tokio::test]
10902    async fn route_rejects_unknown_response_format_type_with_openai_error_param() {
10903        let response = post_json(
10904            router_with_stub("unused"),
10905            "/v1/chat/completions",
10906            json!({
10907                "model": "stub-model",
10908                "messages": [{"role": "user", "content": "hello"}],
10909                "response_format": {"type": "xml"}
10910            }),
10911        )
10912        .await;
10913        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
10914        let body = response_json(response).await;
10915        assert_eq!(body["error"]["type"], "invalid_request_error");
10916        assert_eq!(body["error"]["param"], "response_format.type");
10917    }
10918
10919    #[tokio::test]
10920    async fn route_chat_engine_unavailable_maps_to_503() {
10921        let response = post_json(
10922            router_without_llm(),
10923            "/v1/chat/completions",
10924            json!({
10925                "model": "stub-model",
10926                "messages": [{"role": "user", "content": "hello"}]
10927            }),
10928        )
10929        .await;
10930        assert_eq!(response.status(), AxumStatusCode::SERVICE_UNAVAILABLE);
10931        let body = response_json(response).await;
10932        assert_eq!(body["error"]["type"], "service_unavailable_error");
10933        assert_eq!(body["error"]["param"], Value::Null);
10934    }
10935
10936    #[tokio::test]
10937    async fn route_chat_generation_failure_maps_to_500() {
10938        let response = post_json(
10939            router_with_failing_llm(),
10940            "/v1/chat/completions",
10941            json!({
10942                "model": "failing-model",
10943                "messages": [{"role": "user", "content": "hello"}]
10944            }),
10945        )
10946        .await;
10947        assert_eq!(response.status(), AxumStatusCode::INTERNAL_SERVER_ERROR);
10948        let body = response_json(response).await;
10949        assert_eq!(body["error"]["type"], "internal_server_error");
10950        assert!(body["error"]["message"]
10951            .as_str()
10952            .unwrap()
10953            .contains("stub generation failed"));
10954    }
10955
10956    #[tokio::test]
10957    async fn route_chat_generation_failure_writes_replay_diagnostics() {
10958        let root = unique_request_dump_dir("chat-sync-failure");
10959        let profile = unique_profile_jsonl("chat-sync-failure");
10960        let response = post_json(
10961            router_with_failing_llm_request_dump_and_profile(root.clone(), profile.clone()),
10962            "/v1/chat/completions",
10963            json!({
10964                "model": "failing-model",
10965                "messages": [{"role": "user", "content": "hello"}]
10966            }),
10967        )
10968        .await;
10969        assert_eq!(response.status(), AxumStatusCode::INTERNAL_SERVER_ERROR);
10970        assert_chat_failure_replay_bundle(
10971            &root,
10972            "chat_completions_sync",
10973            "internal",
10974            "stub generation failed",
10975        );
10976        let event = read_profile_events(&profile)
10977            .into_iter()
10978            .find(|event| event["phase"] == "chat_completions_sync")
10979            .expect("sync failure profile event");
10980        assert_eq!(event["event_kind"], "timed_span");
10981        assert_eq!(event["status"], "failure");
10982        assert!(event["duration_us"].as_u64().is_some());
10983        assert_eq!(event["attributes"]["terminal_failure_event"], true);
10984        assert_eq!(event["error"]["kind"], "internal");
10985        let _ = fs::remove_dir_all(root);
10986        let _ = fs::remove_file(profile);
10987    }
10988
10989    #[tokio::test]
10990    async fn route_chat_resource_failure_writes_resource_replay_diagnostics() {
10991        let root = unique_request_dump_dir("chat-resource-failure");
10992        let response = post_json(
10993            router_with_resource_exhausted_llm_and_request_dump_dir(root.clone()),
10994            "/v1/chat/completions",
10995            json!({
10996                "model": "failing-model",
10997                "messages": [{"role": "user", "content": "hello"}]
10998            }),
10999        )
11000        .await;
11001        assert_eq!(response.status(), AxumStatusCode::SERVICE_UNAVAILABLE);
11002        let bundle = only_replay_bundle(&root);
11003        let bad_scan = read_json_file(bundle.join("bad_output_scan.json"));
11004        assert_eq!(bad_scan["failure_kind"], "oom_admission");
11005        let diagnostics = read_json_file(bundle.join("failure_diagnostics.json"));
11006        assert_eq!(diagnostics["failure_kind"], "oom_admission");
11007        assert_eq!(
11008            diagnostics["first_failure_event"]["error_kind"],
11009            "resource_exhausted"
11010        );
11011        assert_eq!(
11012            diagnostics["capacity"]["resource_kind"],
11013            "admission_capacity"
11014        );
11015        assert!(diagnostics["capacity"]["reason"]
11016            .as_str()
11017            .expect("capacity reason")
11018            .contains("admission capacity exhausted"));
11019        assert_eq!(
11020            diagnostics["nearest_resource_event"]["resource_kind"],
11021            "admission_capacity"
11022        );
11023        assert!(diagnostics["nearest_memory_snapshot"]["current_bytes"].is_number());
11024        assert!(diagnostics["nearest_memory_snapshot"]["high_water_bytes"].is_number());
11025        let _ = fs::remove_dir_all(root);
11026    }
11027
11028    #[tokio::test]
11029    async fn route_chat_sync_success_updates_replay_output_tokens() {
11030        let root = unique_request_dump_dir("chat-sync-success-output");
11031        let response = post_json(
11032            router_with_stub_and_request_dump_dir("OK", root.clone()),
11033            "/v1/chat/completions",
11034            json!({
11035                "model": "stub-model",
11036                "messages": [{"role": "user", "content": "hello"}]
11037            }),
11038        )
11039        .await;
11040        assert_eq!(response.status(), AxumStatusCode::OK);
11041        let body = response_json(response).await;
11042        assert_eq!(body["choices"][0]["message"]["content"], "OK");
11043        assert_chat_success_replay_bundle(&root, &[11, 12], "stop", "OK");
11044        let _ = fs::remove_dir_all(root);
11045    }
11046
11047    #[tokio::test]
11048    async fn route_chat_sync_success_writes_product_profile_event() {
11049        let root = unique_request_dump_dir("chat-sync-profile");
11050        let profile = unique_profile_jsonl("chat-sync-profile");
11051        let response = post_json(
11052            router_with_stub_request_dump_and_profile("OK", root.clone(), profile.clone()),
11053            "/v1/chat/completions",
11054            json!({
11055                "model": "stub-model",
11056                "messages": [{"role": "user", "content": "hello"}]
11057            }),
11058        )
11059        .await;
11060        assert_eq!(response.status(), AxumStatusCode::OK);
11061        let _ = response_json(response).await;
11062
11063        let events = read_profile_events(&profile);
11064        assert_eq!(events.len(), 2, "events: {events:#?}");
11065        let event = events
11066            .iter()
11067            .find(|event| event["phase"] == "chat_completions_sync_complete")
11068            .expect("sync completion profile event");
11069        assert_eq!(
11070            event["schema_version"],
11071            OBSERVABILITY_PROFILE_SCHEMA_VERSION
11072        );
11073        assert_eq!(event["entrypoint"], "serve");
11074        assert_eq!(event["event_kind"], "timed_span");
11075        assert_eq!(event["status"], "ok");
11076        assert_eq!(event["phase"], "chat_completions_sync_complete");
11077        assert_eq!(event["attributes"]["actual_model_smoke"], true);
11078        assert_eq!(event["attributes"]["profile_detail"], "latency");
11079        assert_eq!(event["attributes"]["diagnostic_only"], false);
11080        assert_eq!(event["attributes"]["stream"], false);
11081        assert_eq!(event["attributes"]["output_token_count"], 2);
11082        assert_eq!(event["attributes"]["prompt_token_count"], 7);
11083        assert_eq!(event["attributes"]["completion_token_count"], 2);
11084        assert_eq!(event["attributes"]["total_token_count"], 9);
11085        assert_eq!(event["attributes"]["token_count_source"], "usage");
11086        assert_eq!(event["attributes"]["finish_reason"], "stop");
11087        assert_eq!(
11088            event["attributes"]["engine_token_clock_source"],
11089            "rust_std_instant"
11090        );
11091        assert_eq!(event["attributes"]["engine_token_commit_count"], 2);
11092        assert_eq!(event["attributes"]["itl_interval_count"], 1);
11093        assert_eq!(event["attributes"]["ttft_us"], 1_000);
11094        assert_eq!(event["attributes"]["itl_us_avg"], 1_000);
11095        assert!(event["attributes"]["http_first_sse_enqueue_us"].is_null());
11096        assert!(event["duration_us"].as_u64().unwrap_or_default() > 0);
11097        assert!(
11098            event["attributes"]["e2e_duration_us"]
11099                .as_u64()
11100                .unwrap_or_default()
11101                > 0
11102        );
11103        assert_eq!(
11104            event["replay"]["bundle_dir"].as_str(),
11105            Some(root.to_string_lossy().as_ref())
11106        );
11107        assert!(event["replay"]["command"]
11108            .as_str()
11109            .unwrap_or_default()
11110            .contains("replay_body.json"));
11111        let memory_event = events
11112            .iter()
11113            .find(|event| event["phase"] == "actual_serve_first_request_done")
11114            .expect("first request memory profile event");
11115        assert_eq!(memory_event["event_kind"], "memory");
11116        assert_eq!(
11117            memory_event["attributes"]["memory_stage"],
11118            "first_request_done"
11119        );
11120        assert_eq!(
11121            memory_event["attributes"]["memory_measurement"],
11122            "process_rss"
11123        );
11124        assert!(memory_event["memory"]["current_bytes"]
11125            .as_u64()
11126            .is_some_and(|bytes| bytes > 0));
11127        let _ = fs::remove_dir_all(root);
11128        let _ = fs::remove_file(profile);
11129    }
11130
11131    #[tokio::test]
11132    async fn route_chat_profile_events_preserve_benchmark_correlation() {
11133        let root = unique_request_dump_dir("chat-benchmark-correlation");
11134        let profile = unique_profile_jsonl("chat-benchmark-correlation");
11135        let correlation = BenchmarkRequestCorrelation::new(
11136            "bench-123".to_string(),
11137            "cell-1-closed-c8".to_string(),
11138            2,
11139            ferrum_bench_core::BenchmarkPhase::Measured,
11140            17,
11141        )
11142        .unwrap();
11143        let response = post_json_with_benchmark_correlation(
11144            router_with_stub_request_dump_and_profile("OK", root.clone(), profile.clone()),
11145            "/v1/chat/completions",
11146            json!({
11147                "model": "stub-model",
11148                "messages": [{"role": "user", "content": "hello"}]
11149            }),
11150            &correlation,
11151        )
11152        .await;
11153        assert_eq!(response.status(), AxumStatusCode::OK);
11154        let _ = response_json(response).await;
11155
11156        let events = read_profile_events(&profile);
11157        assert_eq!(events.len(), 2, "events: {events:#?}");
11158        for event in events {
11159            assert_eq!(event["attributes"]["benchmark_run_id"], "bench-123");
11160            assert_eq!(event["attributes"]["cell_id"], "cell-1-closed-c8");
11161            assert_eq!(event["attributes"]["repeat_index"], 2);
11162            assert_eq!(event["attributes"]["phase"], "measured");
11163            assert_eq!(event["attributes"]["request_index"], 17);
11164        }
11165        let _ = fs::remove_dir_all(root);
11166        let _ = fs::remove_file(profile);
11167    }
11168
11169    #[tokio::test]
11170    async fn route_chat_rejects_partial_benchmark_correlation_headers() {
11171        let response = router_with_stub("OK")
11172            .oneshot(
11173                Request::builder()
11174                    .method("POST")
11175                    .uri("/v1/chat/completions")
11176                    .header(header::CONTENT_TYPE, "application/json")
11177                    .header(BENCHMARK_RUN_ID_HEADER, "bench-123")
11178                    .body(Body::from(
11179                        json!({
11180                            "model": "stub-model",
11181                            "messages": [{"role": "user", "content": "hello"}]
11182                        })
11183                        .to_string(),
11184                    ))
11185                    .expect("request"),
11186            )
11187            .await
11188            .expect("route response");
11189        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
11190    }
11191
11192    #[tokio::test]
11193    async fn route_chat_sync_profile_jsonl_is_parseable_under_concurrent_requests() {
11194        let root = unique_request_dump_dir("chat-sync-profile-concurrent");
11195        let profile = unique_profile_jsonl("chat-sync-profile-concurrent");
11196        let app = router_with_stub_request_dump_and_profile("OK", root.clone(), profile.clone());
11197
11198        let mut handles = Vec::new();
11199        for request_index in 0..8 {
11200            let app = app.clone();
11201            handles.push(tokio::spawn(async move {
11202                let response = post_json(
11203                    app,
11204                    "/v1/chat/completions",
11205                    json!({
11206                        "model": "stub-model",
11207                        "messages": [{"role": "user", "content": format!("hello {request_index}")}]
11208                    }),
11209                )
11210                .await;
11211                assert_eq!(response.status(), AxumStatusCode::OK);
11212                let body = response_json(response).await;
11213                assert_eq!(body["choices"][0]["message"]["content"], "OK");
11214            }));
11215        }
11216
11217        for handle in handles {
11218            handle.await.expect("concurrent request task");
11219        }
11220
11221        let raw = fs::read_to_string(&profile).expect("profile jsonl");
11222        let mut completion_events = 0usize;
11223        for (line_index, line) in raw
11224            .lines()
11225            .filter(|line| !line.trim().is_empty())
11226            .enumerate()
11227        {
11228            let event: Value = serde_json::from_str(line).unwrap_or_else(|err| {
11229                panic!(
11230                    "profile line {} invalid JSON: {err}: {line}",
11231                    line_index + 1
11232                )
11233            });
11234            if event["phase"] == "chat_completions_sync_complete" {
11235                completion_events += 1;
11236            }
11237        }
11238        assert_eq!(completion_events, 8);
11239        let _ = fs::remove_dir_all(root);
11240        let _ = fs::remove_file(profile);
11241    }
11242
11243    #[tokio::test]
11244    async fn route_chat_stream_success_updates_replay_output_tokens() {
11245        let root = unique_request_dump_dir("chat-stream-success-output");
11246        let response = post_json(
11247            router_with_stub_stream_chunks_and_request_dump_dir(&["O", "K"], root.clone()),
11248            "/v1/chat/completions",
11249            json!({
11250                "model": "stub-model",
11251                "messages": [{"role": "user", "content": "hello"}],
11252                "stream": true
11253            }),
11254        )
11255        .await;
11256        assert_eq!(response.status(), AxumStatusCode::OK);
11257        let body = response_text(response).await;
11258        assert!(body.contains("data: [DONE]"), "body: {body}");
11259        assert_chat_success_replay_bundle(&root, &[11, 12], "stop", "OK");
11260        let _ = fs::remove_dir_all(root);
11261    }
11262
11263    #[tokio::test]
11264    async fn route_chat_stream_success_writes_product_profile_event() {
11265        let root = unique_request_dump_dir("chat-stream-profile");
11266        let profile = unique_profile_jsonl("chat-stream-profile");
11267        let response = post_json(
11268            router_with_stub_stream_request_dump_and_profile(
11269                &["O", "K"],
11270                root.clone(),
11271                profile.clone(),
11272            ),
11273            "/v1/chat/completions",
11274            json!({
11275                "model": "stub-model",
11276                "messages": [{"role": "user", "content": "hello"}],
11277                "stream": true
11278            }),
11279        )
11280        .await;
11281        assert_eq!(response.status(), AxumStatusCode::OK);
11282        let body = response_text(response).await;
11283        assert!(body.contains("data: [DONE]"), "body: {body}");
11284
11285        let events = read_profile_events(&profile);
11286        assert_eq!(events.len(), 2, "events: {events:#?}");
11287        let event = events
11288            .iter()
11289            .find(|event| event["phase"] == "chat_completions_stream_complete")
11290            .expect("stream completion profile event");
11291        assert_eq!(
11292            event["schema_version"],
11293            OBSERVABILITY_PROFILE_SCHEMA_VERSION
11294        );
11295        assert_eq!(event["entrypoint"], "serve");
11296        assert_eq!(event["event_kind"], "timed_span");
11297        assert_eq!(event["status"], "ok");
11298        assert_eq!(event["phase"], "chat_completions_stream_complete");
11299        assert_eq!(event["attributes"]["actual_model_smoke"], true);
11300        assert_eq!(event["attributes"]["profile_detail"], "latency");
11301        assert_eq!(event["attributes"]["diagnostic_only"], false);
11302        assert_eq!(event["attributes"]["stream"], true);
11303        assert_eq!(event["attributes"]["output_token_count"], 2);
11304        assert_eq!(event["attributes"]["prompt_token_count"], 5);
11305        assert_eq!(event["attributes"]["completion_token_count"], 2);
11306        assert_eq!(event["attributes"]["total_token_count"], 7);
11307        assert_eq!(event["attributes"]["token_count_source"], "usage");
11308        assert_eq!(event["attributes"]["finish_reason"], "stop");
11309        assert!(event["duration_us"].as_u64().unwrap_or_default() > 0);
11310        assert!(
11311            event["attributes"]["e2e_duration_us"]
11312                .as_u64()
11313                .unwrap_or_default()
11314                > 0
11315        );
11316        assert!(event["attributes"]["ttft_us"].as_u64().is_some());
11317        assert!(event["attributes"]["itl_us_avg"].as_u64().is_some());
11318        assert_eq!(
11319            event["attributes"]["engine_token_commit_nanos_since_request_start"],
11320            json!([1_000_000, 2_000_000])
11321        );
11322        assert_eq!(event["attributes"]["itl_interval_count"], 1);
11323        assert_eq!(event["attributes"]["itl_source"], "engine_token_commit");
11324        assert!(event["attributes"]["engine_stream_first_chunk_received_us"]
11325            .as_u64()
11326            .is_some());
11327        assert!(event["attributes"]["http_first_sse_enqueue_us"]
11328            .as_u64()
11329            .is_some());
11330        assert!(event["attributes"]["http_stream_flush_unavailable_reason"]
11331            .as_str()
11332            .is_some());
11333        assert_eq!(
11334            event["replay"]["bundle_dir"].as_str(),
11335            Some(root.to_string_lossy().as_ref())
11336        );
11337        let memory_event = events
11338            .iter()
11339            .find(|event| event["phase"] == "actual_serve_first_request_done")
11340            .expect("first request memory profile event");
11341        assert_eq!(memory_event["event_kind"], "memory");
11342        assert_eq!(
11343            memory_event["attributes"]["memory_stage"],
11344            "first_request_done"
11345        );
11346        assert_eq!(
11347            memory_event["attributes"]["memory_measurement"],
11348            "process_rss"
11349        );
11350        assert!(memory_event["memory"]["current_bytes"]
11351            .as_u64()
11352            .is_some_and(|bytes| bytes > 0));
11353        let _ = fs::remove_dir_all(root);
11354        let _ = fs::remove_file(profile);
11355    }
11356
11357    #[tokio::test]
11358    async fn route_chat_stream_profile_retains_non_visible_terminal_token() {
11359        let root = unique_request_dump_dir("chat-stream-profile-terminal-token");
11360        let profile = unique_profile_jsonl("chat-stream-profile-terminal-token");
11361        let llm = StubLlm {
11362            stream_usage: Some(TokenUsage::new(5, 2)),
11363            ..StubLlm::with_stream_chunks(&["Paris"])
11364        };
11365        let app = AxumServer::from_state(
11366            AppState::default()
11367                .with_llm(Arc::new(llm))
11368                .with_request_dump_dir(Some(root.clone()))
11369                .with_profile_detail(ferrum_types::ObservabilityProfileDetail::Latency)
11370                .with_profile_jsonl(Some(profile.clone())),
11371        )
11372        .build_router();
11373
11374        let response = post_json(
11375            app,
11376            "/v1/chat/completions",
11377            json!({
11378                "model": "stub-model",
11379                "messages": [{"role": "user", "content": "hello"}],
11380                "stream": true,
11381                "stream_options": {"include_usage": true}
11382            }),
11383        )
11384        .await;
11385        assert_eq!(response.status(), AxumStatusCode::OK);
11386        let body = response_text(response).await;
11387        assert!(body.contains("\"completion_tokens\":2"), "body: {body}");
11388        assert!(body.contains("data: [DONE]"), "body: {body}");
11389
11390        let events = read_profile_events(&profile);
11391        let event = events
11392            .iter()
11393            .find(|event| event["phase"] == "chat_completions_stream_complete")
11394            .expect("stream completion profile event");
11395        assert_eq!(event["attributes"]["output_token_count"], 2);
11396        assert_eq!(event["attributes"]["completion_token_count"], 2);
11397        assert_eq!(event["attributes"]["engine_token_commit_count"], 2);
11398        assert_chat_success_replay_bundle(&root, &[11, 12], "stop", "Paris");
11399
11400        let _ = fs::remove_dir_all(root);
11401        let _ = fs::remove_file(profile);
11402    }
11403
11404    #[tokio::test]
11405    async fn route_chat_sync_bad_output_updates_replay_classifier() {
11406        let root = unique_request_dump_dir("chat-sync-bad-output");
11407        let response = post_json(
11408            router_with_stub_and_request_dump_dir("<unk>", root.clone()),
11409            "/v1/chat/completions",
11410            json!({
11411                "model": "stub-model",
11412                "messages": [{"role": "user", "content": "hello"}]
11413            }),
11414        )
11415        .await;
11416        assert_eq!(response.status(), AxumStatusCode::OK);
11417        let _ = response_json(response).await;
11418        let bundle = only_replay_bundle(&root);
11419        let bad_scan = read_json_file(bundle.join("bad_output_scan.json"));
11420        assert_eq!(bad_scan["bad_output"], true);
11421        assert_eq!(bad_scan["reasons"], json!(["reserved_token"]));
11422        assert_eq!(bad_scan["first_bad_text_span"]["reason"], "reserved_token");
11423        let _ = fs::remove_dir_all(root);
11424    }
11425
11426    #[tokio::test]
11427    async fn route_chat_stream_generation_failure_emits_openai_error_event() {
11428        let response = post_json(
11429            router_with_failing_llm(),
11430            "/v1/chat/completions",
11431            json!({
11432                "model": "failing-model",
11433                "messages": [{"role": "user", "content": "hello"}],
11434                "stream": true
11435            }),
11436        )
11437        .await;
11438        assert_eq!(response.status(), AxumStatusCode::INTERNAL_SERVER_ERROR);
11439        let body = response_json(response).await;
11440        assert_eq!(body["error"]["type"], "internal_server_error");
11441        assert!(body["error"]["message"]
11442            .as_str()
11443            .unwrap_or_default()
11444            .contains("stub stream failed"));
11445    }
11446
11447    #[tokio::test]
11448    async fn route_chat_stream_generation_failure_writes_replay_diagnostics() {
11449        let root = unique_request_dump_dir("chat-stream-start-failure");
11450        let response = post_json(
11451            router_with_failing_llm_and_request_dump_dir(root.clone()),
11452            "/v1/chat/completions",
11453            json!({
11454                "model": "failing-model",
11455                "messages": [{"role": "user", "content": "hello"}],
11456                "stream": true
11457            }),
11458        )
11459        .await;
11460        assert_eq!(response.status(), AxumStatusCode::INTERNAL_SERVER_ERROR);
11461        let body = response_json(response).await;
11462        assert_eq!(body["error"]["type"], "internal_server_error");
11463        assert!(body["error"]["message"]
11464            .as_str()
11465            .unwrap_or_default()
11466            .contains("stub stream failed"));
11467        assert_chat_failure_replay_bundle(
11468            &root,
11469            "chat_completions_stream_start",
11470            "internal",
11471            "stub stream failed",
11472        );
11473        let _ = fs::remove_dir_all(root);
11474    }
11475
11476    #[tokio::test]
11477    async fn route_chat_stream_chunk_failure_emits_openai_error_event() {
11478        let response = post_json(
11479            router_with_stream_chunk_failing_llm(),
11480            "/v1/chat/completions",
11481            json!({
11482                "model": "failing-model",
11483                "messages": [{"role": "user", "content": "hello"}],
11484                "stream": true
11485            }),
11486        )
11487        .await;
11488        assert_eq!(response.status(), AxumStatusCode::OK);
11489        let body = response_text(response).await;
11490        assert_openai_stream_error(&body, "stub stream chunk failed");
11491    }
11492
11493    #[tokio::test]
11494    async fn route_chat_stream_chunk_failure_writes_replay_diagnostics() {
11495        let root = unique_request_dump_dir("chat-stream-chunk-failure");
11496        let response = post_json(
11497            router_with_stream_chunk_failing_llm_and_request_dump_dir(root.clone()),
11498            "/v1/chat/completions",
11499            json!({
11500                "model": "failing-model",
11501                "messages": [{"role": "user", "content": "hello"}],
11502                "stream": true
11503            }),
11504        )
11505        .await;
11506        assert_eq!(response.status(), AxumStatusCode::OK);
11507        let body = response_text(response).await;
11508        assert_openai_stream_error(&body, "stub stream chunk failed");
11509        assert_chat_failure_replay_bundle(
11510            &root,
11511            "chat_completions_stream_next",
11512            "internal",
11513            "stub stream chunk failed",
11514        );
11515        let _ = fs::remove_dir_all(root);
11516    }
11517
11518    #[tokio::test]
11519    async fn route_completions_engine_unavailable_maps_to_503() {
11520        let response = post_json(
11521            router_without_llm(),
11522            "/v1/completions",
11523            json!({
11524                "model": "stub-model",
11525                "prompt": "complete me"
11526            }),
11527        )
11528        .await;
11529        assert_eq!(response.status(), AxumStatusCode::SERVICE_UNAVAILABLE);
11530        let body = response_json(response).await;
11531        assert_eq!(body["error"]["type"], "service_unavailable_error");
11532        assert_eq!(body["error"]["param"], Value::Null);
11533    }
11534
11535    #[tokio::test]
11536    async fn route_embeddings_engine_unavailable_maps_to_503() {
11537        let response = post_json(
11538            router_without_llm(),
11539            "/v1/embeddings",
11540            json!({
11541                "model": "embed-model",
11542                "input": "hello"
11543            }),
11544        )
11545        .await;
11546        assert_eq!(response.status(), AxumStatusCode::SERVICE_UNAVAILABLE);
11547        let body = response_json(response).await;
11548        assert_eq!(body["error"]["type"], "service_unavailable_error");
11549        assert_eq!(body["error"]["param"], Value::Null);
11550    }
11551
11552    #[tokio::test]
11553    async fn route_embeddings_contract_uses_stub_engine() {
11554        let response = post_json(
11555            router_with_stub_embed(),
11556            "/v1/embeddings",
11557            json!({
11558                "model": "stub-embed",
11559                "input": ["hi", "world"],
11560                "encoding_format": "float"
11561            }),
11562        )
11563        .await;
11564        assert_eq!(response.status(), AxumStatusCode::OK);
11565        let body = response_json(response).await;
11566        assert_eq!(body["object"], "list");
11567        assert_eq!(body["model"], "stub-embed");
11568        assert_eq!(body["usage"]["prompt_tokens"], 7);
11569        assert_eq!(body["usage"]["total_tokens"], 7);
11570
11571        let data = body["data"].as_array().expect("embedding data");
11572        assert_eq!(data.len(), 2, "body: {body}");
11573        assert_eq!(data[0]["object"], "embedding");
11574        assert_eq!(data[0]["index"], 0);
11575        assert_eq!(data[0]["embedding"].as_array().unwrap().len(), 3);
11576        assert_eq!(data[0]["embedding"][0].as_f64().unwrap(), 2.0);
11577        assert_eq!(data[1]["index"], 1);
11578        assert_eq!(data[1]["embedding"][0].as_f64().unwrap(), 5.0);
11579    }
11580
11581    #[tokio::test]
11582    async fn route_embeddings_public_alias_succeeds_and_unknown_alias_is_rejected() {
11583        let registry = ServedModelRegistry::try_new(
11584            "stub-embed",
11585            ServedModelKind::Embedding,
11586            vec!["public-embed".to_string()],
11587            vec![],
11588        )
11589        .unwrap();
11590        let server =
11591            AxumServer::from_embed(Arc::new(StubEmbed::new())).with_served_model_registry(registry);
11592        let accepted = post_json(
11593            server.build_router(),
11594            "/v1/embeddings",
11595            json!({"model": "public-embed", "input": "hello"}),
11596        )
11597        .await;
11598        assert_eq!(accepted.status(), AxumStatusCode::OK);
11599        assert_eq!(response_json(accepted).await["model"], "public-embed");
11600
11601        let rejected = post_json(
11602            server.build_router(),
11603            "/v1/embeddings",
11604            json!({"model": "stub-embed", "input": "hello"}),
11605        )
11606        .await;
11607        assert_eq!(rejected.status(), AxumStatusCode::BAD_REQUEST);
11608        let body = response_json(rejected).await;
11609        assert_eq!(body["error"]["type"], "invalid_request_error");
11610        assert_eq!(body["error"]["param"], "model");
11611    }
11612
11613    #[tokio::test]
11614    async fn route_embeddings_rejects_unsupported_encoding_format() {
11615        let response = post_json(
11616            router_with_stub_embed(),
11617            "/v1/embeddings",
11618            json!({
11619                "model": "stub-embed",
11620                "input": "hi",
11621                "encoding_format": "base64"
11622            }),
11623        )
11624        .await;
11625        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
11626        let body = response_json(response).await;
11627        assert_eq!(body["error"]["type"], "invalid_request_error");
11628        assert_eq!(body["error"]["param"], "encoding_format");
11629    }
11630
11631    #[tokio::test]
11632    async fn route_embeddings_rejects_empty_input_with_field_param() {
11633        let response = post_json(
11634            router_with_stub_embed(),
11635            "/v1/embeddings",
11636            json!({
11637                "model": "stub-embed",
11638                "input": []
11639            }),
11640        )
11641        .await;
11642        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
11643        let body = response_json(response).await;
11644        assert_eq!(body["error"]["type"], "invalid_request_error");
11645        assert_eq!(body["error"]["param"], "input");
11646    }
11647
11648    #[tokio::test]
11649    async fn route_embeddings_rejects_empty_item_with_field_param() {
11650        let response = post_json(
11651            router_with_stub_embed(),
11652            "/v1/embeddings",
11653            json!({
11654                "model": "stub-embed",
11655                "input": [{}]
11656            }),
11657        )
11658        .await;
11659        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
11660        let body = response_json(response).await;
11661        assert_eq!(body["error"]["type"], "invalid_request_error");
11662        assert_eq!(body["error"]["param"], "input");
11663    }
11664
11665    #[tokio::test]
11666    async fn route_embeddings_invalid_json_maps_to_openai_error() {
11667        let response = post_raw_json(router_with_stub_embed(), "/v1/embeddings", "{").await;
11668        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
11669        let body = response_json(response).await;
11670        assert_eq!(body["error"]["type"], "invalid_request_error");
11671        assert_eq!(body["error"]["param"], Value::Null);
11672        assert!(body["error"]["message"]
11673            .as_str()
11674            .unwrap()
11675            .contains("invalid embeddings request"));
11676    }
11677
11678    #[tokio::test]
11679    async fn route_transcriptions_engine_unavailable_maps_to_503() {
11680        let boundary = "ferrum-test-boundary";
11681        let body = concat!(
11682            "--ferrum-test-boundary\r\n",
11683            "Content-Disposition: form-data; name=\"file\"; filename=\"audio.wav\"\r\n",
11684            "Content-Type: audio/wav\r\n",
11685            "\r\n",
11686            "RIFFtest\r\n",
11687            "--ferrum-test-boundary--\r\n"
11688        );
11689        let response = post_multipart(
11690            router_without_llm(),
11691            "/v1/audio/transcriptions",
11692            boundary,
11693            body,
11694        )
11695        .await;
11696        assert_eq!(response.status(), AxumStatusCode::SERVICE_UNAVAILABLE);
11697        let body = response_json(response).await;
11698        assert_eq!(body["error"]["type"], "service_unavailable_error");
11699        assert_eq!(body["error"]["param"], Value::Null);
11700    }
11701
11702    #[tokio::test]
11703    async fn route_transcriptions_contract_uses_stub_engine() {
11704        let boundary = "ferrum-test-boundary";
11705        let body = concat!(
11706            "--ferrum-test-boundary\r\n",
11707            "Content-Disposition: form-data; name=\"file\"; filename=\"audio.wav\"\r\n",
11708            "Content-Type: audio/wav\r\n",
11709            "\r\n",
11710            "RIFFtest\r\n",
11711            "--ferrum-test-boundary\r\n",
11712            "Content-Disposition: form-data; name=\"language\"\r\n",
11713            "\r\n",
11714            "en\r\n",
11715            "--ferrum-test-boundary\r\n",
11716            "Content-Disposition: form-data; name=\"response_format\"\r\n",
11717            "\r\n",
11718            "json\r\n",
11719            "--ferrum-test-boundary--\r\n"
11720        );
11721        let response = post_multipart(
11722            router_with_stub_transcribe(),
11723            "/v1/audio/transcriptions",
11724            boundary,
11725            body,
11726        )
11727        .await;
11728        assert_eq!(response.status(), AxumStatusCode::OK);
11729        let body = response_json(response).await;
11730        assert_eq!(body["text"], "bytes:8:en");
11731    }
11732
11733    #[tokio::test]
11734    async fn route_transcriptions_rejects_unsupported_response_format() {
11735        let boundary = "ferrum-test-boundary";
11736        let body = concat!(
11737            "--ferrum-test-boundary\r\n",
11738            "Content-Disposition: form-data; name=\"file\"; filename=\"audio.wav\"\r\n",
11739            "Content-Type: audio/wav\r\n",
11740            "\r\n",
11741            "RIFFtest\r\n",
11742            "--ferrum-test-boundary\r\n",
11743            "Content-Disposition: form-data; name=\"response_format\"\r\n",
11744            "\r\n",
11745            "text\r\n",
11746            "--ferrum-test-boundary--\r\n"
11747        );
11748        let response = post_multipart(
11749            router_with_stub_transcribe(),
11750            "/v1/audio/transcriptions",
11751            boundary,
11752            body,
11753        )
11754        .await;
11755        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
11756        let body = response_json(response).await;
11757        assert_eq!(body["error"]["type"], "invalid_request_error");
11758        assert_eq!(body["error"]["param"], "response_format");
11759    }
11760
11761    #[tokio::test]
11762    async fn route_transcriptions_rejects_missing_file_with_field_param() {
11763        let boundary = "ferrum-test-boundary";
11764        let body = concat!(
11765            "--ferrum-test-boundary\r\n",
11766            "Content-Disposition: form-data; name=\"language\"\r\n",
11767            "\r\n",
11768            "en\r\n",
11769            "--ferrum-test-boundary--\r\n"
11770        );
11771        let response = post_multipart(
11772            router_with_stub_transcribe(),
11773            "/v1/audio/transcriptions",
11774            boundary,
11775            body,
11776        )
11777        .await;
11778        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
11779        let body = response_json(response).await;
11780        assert_eq!(body["error"]["type"], "invalid_request_error");
11781        assert_eq!(body["error"]["param"], "file");
11782    }
11783
11784    #[tokio::test]
11785    async fn route_transcriptions_invalid_multipart_maps_to_openai_error() {
11786        let response = post_json(
11787            router_with_stub_transcribe(),
11788            "/v1/audio/transcriptions",
11789            json!({}),
11790        )
11791        .await;
11792        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
11793        let body = response_json(response).await;
11794        assert_eq!(body["error"]["type"], "invalid_request_error");
11795        assert_eq!(body["error"]["param"], Value::Null);
11796        assert!(body["error"]["message"]
11797            .as_str()
11798            .unwrap()
11799            .contains("invalid transcriptions request"));
11800    }
11801
11802    #[tokio::test]
11803    async fn route_speech_engine_unavailable_maps_to_503() {
11804        let response = post_json(
11805            router_without_llm(),
11806            "/v1/audio/speech",
11807            json!({
11808                "model": "tts-model",
11809                "input": "hello",
11810                "voice": "default"
11811            }),
11812        )
11813        .await;
11814        assert_eq!(response.status(), AxumStatusCode::SERVICE_UNAVAILABLE);
11815        let body = response_json(response).await;
11816        assert_eq!(body["error"]["type"], "service_unavailable_error");
11817        assert_eq!(body["error"]["param"], Value::Null);
11818    }
11819
11820    #[tokio::test]
11821    async fn route_speech_contract_uses_stub_engine() {
11822        let response = post_json(
11823            router_with_stub_tts(),
11824            "/v1/audio/speech",
11825            json!({
11826                "model": "stub-tts",
11827                "input": "hello",
11828                "voice": "default",
11829                "response_format": "wav",
11830                "language": "english"
11831            }),
11832        )
11833        .await;
11834        assert_eq!(response.status(), AxumStatusCode::OK);
11835        assert_eq!(
11836            response.headers().get(header::CONTENT_TYPE).unwrap(),
11837            "audio/wav"
11838        );
11839        let body = response_bytes(response).await;
11840        assert!(body.len() > 44, "WAV should include header and PCM data");
11841        assert_eq!(&body[0..4], b"RIFF");
11842        assert_eq!(&body[8..12], b"WAVE");
11843    }
11844
11845    #[tokio::test]
11846    async fn route_speech_streaming_contract_uses_stub_engine() {
11847        let response = post_json(
11848            router_with_stub_tts(),
11849            "/v1/audio/speech",
11850            json!({
11851                "model": "stub-tts",
11852                "input": "hello",
11853                "voice": "default",
11854                "response_format": "wav",
11855                "stream": true
11856            }),
11857        )
11858        .await;
11859        assert_eq!(response.status(), AxumStatusCode::OK);
11860        assert_eq!(
11861            response.headers().get(header::CONTENT_TYPE).unwrap(),
11862            "audio/wav"
11863        );
11864        assert_eq!(
11865            response.headers().get(header::TRANSFER_ENCODING).unwrap(),
11866            "chunked"
11867        );
11868        let body = response_bytes(response).await;
11869        assert!(body.len() > 44, "streaming WAV should include audio bytes");
11870        assert_eq!(&body[0..4], b"RIFF");
11871        assert_eq!(&body[8..12], b"WAVE");
11872    }
11873
11874    #[tokio::test]
11875    async fn route_speech_pcm_response_format_returns_raw_pcm() {
11876        let response = post_json(
11877            router_with_stub_tts(),
11878            "/v1/audio/speech",
11879            json!({
11880                "model": "stub-tts",
11881                "input": "hello",
11882                "voice": "default",
11883                "response_format": "pcm"
11884            }),
11885        )
11886        .await;
11887        assert_eq!(response.status(), AxumStatusCode::OK);
11888        assert_eq!(
11889            response.headers().get(header::CONTENT_TYPE).unwrap(),
11890            "audio/pcm"
11891        );
11892        let body = response_bytes(response).await;
11893        assert_eq!(body.len(), 6, "three f32 samples should encode as s16le");
11894        assert_eq!(&body[0..2], &[0, 0]);
11895        assert_ne!(&body[0..4], b"RIFF");
11896    }
11897
11898    #[tokio::test]
11899    async fn route_speech_rejects_unsupported_response_format() {
11900        let response = post_json(
11901            router_with_stub_tts(),
11902            "/v1/audio/speech",
11903            json!({
11904                "model": "stub-tts",
11905                "input": "hello",
11906                "voice": "default",
11907                "response_format": "mp3"
11908            }),
11909        )
11910        .await;
11911        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
11912        let body = response_json(response).await;
11913        assert_eq!(body["error"]["type"], "invalid_request_error");
11914        assert_eq!(body["error"]["param"], "response_format");
11915    }
11916
11917    #[tokio::test]
11918    async fn route_speech_invalid_json_maps_to_openai_error() {
11919        let response = post_raw_json(router_with_stub_tts(), "/v1/audio/speech", "{").await;
11920        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
11921        let body = response_json(response).await;
11922        assert_eq!(body["error"]["type"], "invalid_request_error");
11923        assert_eq!(body["error"]["param"], Value::Null);
11924        assert!(body["error"]["message"]
11925            .as_str()
11926            .unwrap()
11927            .contains("invalid speech request"));
11928    }
11929
11930    #[tokio::test]
11931    async fn route_completions_generation_failure_maps_to_500() {
11932        let response = post_json(
11933            router_with_failing_llm(),
11934            "/v1/completions",
11935            json!({
11936                "model": "failing-model",
11937                "prompt": "complete me"
11938            }),
11939        )
11940        .await;
11941        assert_eq!(response.status(), AxumStatusCode::INTERNAL_SERVER_ERROR);
11942        let body = response_json(response).await;
11943        assert_eq!(body["error"]["type"], "internal_server_error");
11944        assert!(body["error"]["message"]
11945            .as_str()
11946            .unwrap()
11947            .contains("stub generation failed"));
11948    }
11949
11950    #[tokio::test]
11951    async fn route_completions_stream_generation_failure_emits_openai_error_event() {
11952        let response = post_json(
11953            router_with_failing_llm(),
11954            "/v1/completions",
11955            json!({
11956                "model": "failing-model",
11957                "prompt": "complete me",
11958                "stream": true
11959            }),
11960        )
11961        .await;
11962        assert_eq!(response.status(), AxumStatusCode::OK);
11963        let body = response_text(response).await;
11964        assert_openai_stream_error(&body, "stub stream failed");
11965    }
11966
11967    #[tokio::test]
11968    async fn route_completions_stream_chunk_failure_emits_openai_error_event() {
11969        let response = post_json(
11970            router_with_stream_chunk_failing_llm(),
11971            "/v1/completions",
11972            json!({
11973                "model": "failing-model",
11974                "prompt": "complete me",
11975                "stream": true
11976            }),
11977        )
11978        .await;
11979        assert_eq!(response.status(), AxumStatusCode::OK);
11980        let body = response_text(response).await;
11981        assert_openai_stream_error(&body, "stub stream chunk failed");
11982    }
11983
11984    #[tokio::test]
11985    async fn route_completions_contract_uses_stub_engine() {
11986        let response = post_json(
11987            router_with_stub("done"),
11988            "/v1/completions",
11989            json!({
11990                "model": "stub-model",
11991                "prompt": "complete me",
11992                "max_tokens": 8,
11993                "temperature": 0.0
11994            }),
11995        )
11996        .await;
11997        assert_eq!(response.status(), AxumStatusCode::OK);
11998        let body = response_json(response).await;
11999        assert_eq!(body["object"], "text_completion");
12000        assert_eq!(body["choices"][0]["text"], "done");
12001        assert_eq!(body["usage"]["prompt_tokens"], 7);
12002        assert_eq!(body["usage"]["completion_tokens"], 2);
12003    }
12004
12005    #[tokio::test]
12006    async fn route_completions_public_alias_maps_to_internal_model() {
12007        let engine = Arc::new(CapturingLlm::new());
12008        let registry = ServedModelRegistry::try_new(
12009            "qwen3",
12010            ServedModelKind::Llm,
12011            vec!["served-alias".to_string()],
12012            vec![],
12013        )
12014        .unwrap();
12015        let router = AxumServer::from_llm(engine.clone())
12016            .with_served_model_registry(registry)
12017            .build_router();
12018        let response = post_json(
12019            router,
12020            "/v1/completions",
12021            json!({"model": "served-alias", "prompt": "complete me"}),
12022        )
12023        .await;
12024
12025        assert_eq!(response.status(), AxumStatusCode::OK);
12026        assert_eq!(response_json(response).await["model"], "served-alias");
12027        assert_eq!(engine.last_request().model_id, ModelId::new("qwen3"));
12028    }
12029
12030    #[tokio::test]
12031    async fn route_completions_streaming_contract_uses_stub_engine() {
12032        let response = post_json(
12033            router_with_stub("done"),
12034            "/v1/completions",
12035            json!({
12036                "model": "stub-model",
12037                "prompt": "complete me",
12038                "max_tokens": 8,
12039                "temperature": 0.0,
12040                "stream": true
12041            }),
12042        )
12043        .await;
12044        assert_eq!(response.status(), AxumStatusCode::OK);
12045        let body = response_text(response).await;
12046        assert!(body.contains("data: [DONE]"), "missing DONE: {body}");
12047        assert!(
12048            body.contains("\"object\":\"text_completion\""),
12049            "missing completion chunk: {body}"
12050        );
12051        assert!(body.contains("\"text\":\"done\""), "missing text: {body}");
12052        assert!(
12053            body.contains("\"choices\":[],\"usage\""),
12054            "missing separate usage chunk: {body}"
12055        );
12056        assert!(
12057            body.contains("\"prompt_tokens\":5"),
12058            "stream usage should come from engine token usage: {body}"
12059        );
12060        assert!(
12061            body.contains("\"completion_tokens\":1"),
12062            "stream completion usage should come from engine token usage: {body}"
12063        );
12064    }
12065
12066    #[tokio::test]
12067    async fn route_completions_stream_waits_for_separate_final_usage_at_max_tokens() {
12068        let response = post_json(
12069            router_with_stub_separate_final_stream_chunk(&["do", "ne"]),
12070            "/v1/completions",
12071            json!({
12072                "model": "stub-model",
12073                "prompt": "complete me",
12074                "max_tokens": 2,
12075                "temperature": 0.0,
12076                "stream": true
12077            }),
12078        )
12079        .await;
12080        assert_eq!(response.status(), AxumStatusCode::OK);
12081        let body = response_text(response).await;
12082        assert_eq!(body.matches("data: [DONE]").count(), 1, "body: {body}");
12083        assert!(
12084            body.contains("\"text\":\"do\""),
12085            "missing first chunk: {body}"
12086        );
12087        assert!(
12088            body.contains("\"text\":\"ne\""),
12089            "missing second chunk: {body}"
12090        );
12091        assert!(
12092            body.contains("\"choices\":[],\"usage\""),
12093            "missing separate usage chunk from final engine chunk: {body}"
12094        );
12095        assert!(
12096            body.contains("\"prompt_tokens\":5"),
12097            "stream usage should come from engine final usage: {body}"
12098        );
12099    }
12100
12101    #[tokio::test]
12102    async fn route_completions_invalid_json_maps_to_openai_error() {
12103        let response = post_raw_json(router_with_stub("unused"), "/v1/completions", "{").await;
12104        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
12105        let body = response_json(response).await;
12106        assert_eq!(body["error"]["type"], "invalid_request_error");
12107        assert_eq!(body["error"]["param"], Value::Null);
12108        assert!(body["error"]["message"]
12109            .as_str()
12110            .unwrap()
12111            .contains("invalid completions request"));
12112    }
12113
12114    #[tokio::test]
12115    async fn route_completions_rejects_unsupported_fields_explicitly() {
12116        for (extra, param) in [
12117            (json!({"n": 2}), "n"),
12118            (json!({"logprobs": 3}), "logprobs"),
12119            (json!({"logit_bias": {"42": 1.0}}), "logit_bias"),
12120        ] {
12121            let mut body = json!({
12122                "model": "stub-model",
12123                "prompt": "complete me"
12124            });
12125            body.as_object_mut()
12126                .expect("object")
12127                .extend(extra.as_object().expect("extra object").clone());
12128            let response = post_json(router_with_stub("unused"), "/v1/completions", body).await;
12129            assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
12130            let body = response_json(response).await;
12131            assert_eq!(body["error"]["type"], "invalid_request_error");
12132            assert_eq!(body["error"]["param"], param);
12133        }
12134    }
12135
12136    #[tokio::test]
12137    async fn streaming_completions_do_not_synthesize_whitespace_usage() {
12138        let response = post_json(
12139            router_with_stub_without_stream_usage("done"),
12140            "/v1/completions",
12141            json!({
12142                "model": "stub-model",
12143                "prompt": "one two three four",
12144                "stream": true
12145            }),
12146        )
12147        .await;
12148        assert_eq!(response.status(), AxumStatusCode::OK);
12149        let body = response_text(response).await;
12150        assert!(body.contains("data: [DONE]"), "missing DONE: {body}");
12151        assert!(
12152            !body.contains("\"usage\":{\"prompt_tokens\""),
12153            "server must not synthesize whitespace-count completion usage: {body}"
12154        );
12155    }
12156
12157    #[tokio::test]
12158    async fn chat_rejects_n_not_one_with_openai_error_param() {
12159        let request = chat_request(json!({"n": 2}));
12160        let err = chat_completions_handler(
12161            State(state_with_stub("unused")),
12162            HeaderMap::new(),
12163            Ok(Json(request)),
12164        )
12165        .await
12166        .expect_err("n=2 should reject");
12167        let (status, body) = error_json(err).await;
12168        assert_eq!(status, AxumStatusCode::BAD_REQUEST);
12169        assert_eq!(body["error"]["type"], "invalid_request_error");
12170        assert_eq!(body["error"]["param"], "n");
12171    }
12172
12173    #[tokio::test]
12174    async fn chat_rejects_logit_bias_and_logprobs_explicitly() {
12175        for (extra, param) in [
12176            (json!({"logit_bias": {"1": 100.0}}), "logit_bias"),
12177            (json!({"logprobs": true}), "logprobs"),
12178            (json!({"top_logprobs": 2}), "top_logprobs"),
12179        ] {
12180            let request = chat_request(extra);
12181            let err = chat_completions_handler(
12182                State(state_with_stub("unused")),
12183                HeaderMap::new(),
12184                Ok(Json(request)),
12185            )
12186            .await
12187            .expect_err("unsupported field should reject");
12188            let (status, body) = error_json(err).await;
12189            assert_eq!(status, AxumStatusCode::BAD_REQUEST);
12190            assert_eq!(body["error"]["param"], param);
12191            assert_eq!(body["error"]["type"], "invalid_request_error");
12192        }
12193    }
12194
12195    #[tokio::test]
12196    async fn chat_stream_options_include_usage_controls_stream_usage() {
12197        let request = chat_request(json!({
12198            "stream": true,
12199            "stream_options": {"include_usage": true}
12200        }));
12201        let response = chat_completions_handler(
12202            State(state_with_stub("ok")),
12203            HeaderMap::new(),
12204            Ok(Json(request)),
12205        )
12206        .await
12207        .expect("stream response");
12208        assert_eq!(response.status(), AxumStatusCode::OK);
12209        let body = response_text(response).await;
12210        assert!(body.contains("data: [DONE]"), "missing DONE: {body}");
12211        assert!(
12212            body.contains("\"usage\"") && body.contains("\"completion_tokens\":1"),
12213            "include_usage=true should emit stream usage: {body}"
12214        );
12215        assert!(
12216            body.contains("\"choices\":[],\"usage\""),
12217            "include_usage=true should use a separate usage chunk: {body}"
12218        );
12219        assert!(
12220            body.contains("\"prompt_tokens\":5"),
12221            "stream usage should come from engine token usage: {body}"
12222        );
12223
12224        let request = chat_request(json!({"stream": true}));
12225        let response = chat_completions_handler(
12226            State(state_with_stub("ok")),
12227            HeaderMap::new(),
12228            Ok(Json(request)),
12229        )
12230        .await
12231        .expect("stream response");
12232        let body = response_text(response).await;
12233        assert!(
12234            !body.contains("\"usage\":{\"prompt_tokens\""),
12235            "stream usage should be omitted unless requested: {body}"
12236        );
12237    }
12238
12239    #[tokio::test]
12240    async fn streaming_chat_does_not_synthesize_whitespace_usage() {
12241        let response = post_json(
12242            router_with_stub_without_stream_usage("ok"),
12243            "/v1/chat/completions",
12244            json!({
12245                "model": "stub-model",
12246                "messages": [{"role": "user", "content": "one two three four"}],
12247                "stream": true,
12248                "stream_options": {"include_usage": true}
12249            }),
12250        )
12251        .await;
12252        assert_eq!(response.status(), AxumStatusCode::OK);
12253        let body = response_text(response).await;
12254        assert!(body.contains("data: [DONE]"), "missing DONE: {body}");
12255        assert!(
12256            !body.contains("\"usage\":{\"prompt_tokens\""),
12257            "server must not synthesize whitespace-count usage when engine stream omits usage: {body}"
12258        );
12259    }
12260
12261    #[test]
12262    fn tool_requests_and_tool_messages_parse_into_structured_api_request() {
12263        let request: ChatCompletionsRequest = serde_json::from_value(json!({
12264            "model": "qwen3",
12265            "messages": [
12266                {"role": "user", "content": "Use the weather tool."},
12267                {
12268                    "role": "assistant",
12269                    "content": null,
12270                    "tool_calls": [{
12271                        "id": "call_1",
12272                        "type": "function",
12273                        "function": {"name": "weather", "arguments": "{\"city\":\"Paris\"}"}
12274                    }]
12275                },
12276                {"role": "tool", "tool_call_id": "call_1", "content": "sunny"}
12277            ],
12278            "tools": [{
12279                "type": "function",
12280                "function": {
12281                    "name": "weather",
12282                    "description": "Get weather",
12283                    "parameters": {
12284                        "type": "object",
12285                        "properties": {"city": {"type": "string"}},
12286                        "required": ["city"]
12287                    }
12288                }
12289            }],
12290            "tool_choice": "auto"
12291        }))
12292        .expect("tool request parses");
12293
12294        validate_chat_request(&request).expect("tool request validates");
12295        let internal = convert_chat_request(&request).expect("convert");
12296        assert!(internal.prompt.contains("\"tools\":[{"));
12297        assert!(internal.prompt.contains("\"type\":\"function\""));
12298        assert!(internal.prompt.contains("\"name\":\"weather\""));
12299        assert!(internal.prompt.contains("<|im_start|>assistant\n{"));
12300        assert!(internal.prompt.contains("\"tool_calls\":[{"));
12301        assert!(internal.prompt.contains("\"id\":\"call_1\""));
12302        assert!(internal
12303            .prompt
12304            .contains("<|im_start|>tool\nsunny<|im_end|>"));
12305        assert_eq!(
12306            internal.metadata["openai_tools"][0]["function"]["name"],
12307            "weather"
12308        );
12309        assert_eq!(internal.metadata["openai_tool_choice"], "auto");
12310        let Some(ferrum_types::ApiRequest::Chat(api)) = internal.api_request.as_ref() else {
12311            panic!("expected structured chat api_request");
12312        };
12313        assert_eq!(api.messages.len(), 3);
12314        assert_eq!(api.messages[2].role, ferrum_types::ApiMessageRole::Tool);
12315        assert_eq!(api.messages[2].content, "sunny");
12316        assert_eq!(api.messages[2].tool_call_id.as_deref(), Some("call_1"));
12317        assert_eq!(api.tools[0].function.name, "weather");
12318        assert_eq!(
12319            api.tool_choice,
12320            Some(ferrum_types::ApiToolChoice::Mode("auto".into()))
12321        );
12322        assert_eq!(
12323            api.messages[1].tool_calls[0].function.arguments,
12324            "{\"city\":\"Paris\"}"
12325        );
12326    }
12327
12328    #[test]
12329    fn omitted_tool_choice_defaults_to_auto_when_tools_are_present() {
12330        let request: ChatCompletionsRequest = serde_json::from_value(json!({
12331            "model": "served-alias",
12332            "messages": [{"role": "user", "content": "Use the weather tool."}],
12333            "tools": [{
12334                "type": "function",
12335                "function": {
12336                    "name": "weather",
12337                    "description": "Get weather",
12338                    "parameters": {
12339                        "type": "object",
12340                        "properties": {"city": {"type": "string"}},
12341                        "required": ["city"]
12342                    }
12343                }
12344            }]
12345        }))
12346        .expect("tool request parses");
12347
12348        validate_chat_request(&request).expect("tool request validates");
12349        let internal = convert_chat_request(&request).expect("convert");
12350        assert!(internal.prompt.contains("\"tools\":[{"));
12351        assert!(internal.prompt.contains("\"tool_choice\":\"auto\""));
12352        assert_eq!(internal.metadata["openai_tool_choice"], "auto");
12353        let initial_forbidden = internal.metadata[INITIAL_FORBIDDEN_TOKEN_TEXTS_METADATA_KEY]
12354            .as_array()
12355            .expect("initial forbidden token list");
12356        assert_eq!(initial_forbidden, &[serde_json::json!(THINK_END_TAG)]);
12357        assert_eq!(
12358            internal.sampling_params.response_format,
12359            ferrum_types::ResponseFormat::Text,
12360            "auto tool choice must preserve native model selection instead of forcing arguments JSON",
12361        );
12362        let Some(ferrum_types::ApiRequest::Chat(api)) = internal.api_request.as_ref() else {
12363            panic!("expected structured chat api_request");
12364        };
12365        assert_eq!(
12366            api.tool_choice,
12367            Some(ferrum_types::ApiToolChoice::Mode("auto".into()))
12368        );
12369    }
12370
12371    #[test]
12372    fn omitted_tool_choice_uses_native_template_protocol_without_hard_schema() {
12373        let request: ChatCompletionsRequest = serde_json::from_value(json!({
12374            "model": "served-alias",
12375            "messages": [{"role": "user", "content": "北京现在天气怎么样?用摄氏度。"}],
12376            "tools": [{
12377                "type": "function",
12378                "function": {
12379                    "name": "get_weather",
12380                    "description": "查询指定城市的当前天气",
12381                    "parameters": {
12382                        "type": "object",
12383                        "properties": {
12384                            "city": {"type": "string"},
12385                            "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
12386                        },
12387                        "required": ["city"]
12388                    }
12389                }
12390            }]
12391        }))
12392        .expect("tool request parses");
12393        let template = ModelChatTemplate::new(
12394            "{% if tools %}<tools>{{ tools | tojson }}</tools>Use <tool_call><function=name><parameter=key>value</parameter></function></tool_call>{% endif %}{% for message in messages %}{{ message.content }}{% endfor %}",
12395            "function-parameter-xml-template",
12396        );
12397
12398        validate_chat_request(&request).expect("tool request validates");
12399        let internal =
12400            convert_chat_request_with_template_model(&request, "served-alias", Some(&template))
12401                .expect("convert");
12402        assert_eq!(internal.metadata["openai_tool_choice"], "auto");
12403        assert_eq!(
12404            internal.sampling_params.response_format,
12405            ferrum_types::ResponseFormat::Text,
12406        );
12407        let Some(ferrum_types::ApiRequest::Chat(api)) = internal.api_request else {
12408            panic!("expected chat API request");
12409        };
12410        assert_eq!(
12411            api.tool_call_protocol,
12412            ferrum_types::ApiToolCallProtocol::FunctionParameterXml,
12413        );
12414    }
12415
12416    #[test]
12417    fn tool_schema_response_format_bounds_unconstrained_strings() {
12418        let request: ChatCompletionsRequest = serde_json::from_value(json!({
12419            "model": "served-alias",
12420            "messages": [{"role": "user", "content": "Use the selected tool."}],
12421            "tools": [{
12422                "type": "function",
12423                "function": {
12424                    "name": "get_weather",
12425                    "parameters": {
12426                        "type": "object",
12427                        "properties": {
12428                            "city": {"type": "string"},
12429                            "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
12430                        },
12431                        "required": ["city"]
12432                    }
12433                }
12434            }],
12435            "tool_choice": {
12436                "type": "function",
12437                "function": {"name": "get_weather"}
12438            }
12439        }))
12440        .expect("tool request parses");
12441
12442        validate_chat_request(&request).expect("tool request validates");
12443        let internal = convert_chat_request(&request).expect("convert");
12444        match internal.sampling_params.response_format {
12445            ferrum_types::ResponseFormat::JsonSchema(ref schema) => {
12446                let value: serde_json::Value =
12447                    serde_json::from_str(schema).expect("schema should be JSON");
12448                assert_eq!(
12449                    value["properties"]["city"]["maxLength"],
12450                    DEFAULT_GUIDED_TOOL_ARGUMENT_STRING_MAX_LENGTH
12451                );
12452                assert_eq!(
12453                    value["properties"]["unit"]["enum"],
12454                    json!(["celsius", "fahrenheit"])
12455                );
12456                assert!(
12457                    value["properties"]["unit"]["maxLength"].is_null(),
12458                    "enum string should remain finite via enum instead of maxLength: {value}"
12459                );
12460            }
12461            ref other => panic!("expected forced tool json schema, got {other:?}"),
12462        }
12463    }
12464
12465    #[test]
12466    fn harmony_named_tool_choice_preserves_native_protocol_envelope() {
12467        let request: ChatCompletionsRequest = serde_json::from_value(json!({
12468            "model": "gpt-oss-20b-mxfp4",
12469            "messages": [{
12470                "role": "user",
12471                "content": "Call get_weather exactly once with city set to Paris."
12472            }],
12473            "tools": [{
12474                "type": "function",
12475                "function": {
12476                    "name": "get_weather",
12477                    "parameters": {
12478                        "type": "object",
12479                        "properties": {"city": {"type": "string"}},
12480                        "required": ["city"],
12481                        "additionalProperties": false
12482                    }
12483                }
12484            }],
12485            "tool_choice": {
12486                "type": "function",
12487                "function": {"name": "get_weather"}
12488            }
12489        }))
12490        .expect("Harmony tool request parses");
12491        let mut template = ModelChatTemplate::new(
12492            "{% if tools %}<|start|>developer<|message|>{{ tools | tojson }}<|end|>{% endif %}{% for message in messages %}<|start|>{{ message.role }}<|message|>{{ message.content }}<|end|>{% endfor %}{% if add_generation_prompt %}<|start|>assistant{% endif %}",
12493            "harmony-tool-template",
12494        );
12495        template.output_protocol = ModelOutputProtocol::HarmonyGptOss;
12496
12497        validate_chat_request(&request).expect("Harmony tool request validates");
12498        let internal =
12499            convert_chat_request_with_template_model(&request, "gpt-oss-20b", Some(&template))
12500                .expect("convert Harmony tool request");
12501
12502        assert!(internal.prompt.ends_with("<|start|>assistant"));
12503        assert_eq!(
12504            internal.sampling_params.model_output_protocol,
12505            ModelOutputProtocol::HarmonyGptOss
12506        );
12507        assert_eq!(
12508            internal.sampling_params.response_format,
12509            ferrum_types::ResponseFormat::Text,
12510            "Harmony must generate its channel/message/call envelope before tool arguments"
12511        );
12512        assert_eq!(
12513            internal.metadata[INITIAL_FORBIDDEN_TOKEN_TEXTS_METADATA_KEY],
12514            json!([]),
12515            "Harmony declares no think delimiter and must not receive the generic structured-call mask"
12516        );
12517        let Some(ferrum_types::ApiRequest::Chat(api)) = internal.api_request else {
12518            panic!("expected chat API request");
12519        };
12520        assert_eq!(
12521            api.tool_choice,
12522            Some(ferrum_types::ApiToolChoice::Function {
12523                tool_type: "function".to_string(),
12524                function: ferrum_types::ApiToolChoiceFunction {
12525                    name: "get_weather".to_string(),
12526                },
12527            })
12528        );
12529    }
12530
12531    #[test]
12532    fn required_tool_choice_uses_tool_schema_response_format_without_extra_prompt_instruction() {
12533        let request: ChatCompletionsRequest = serde_json::from_value(json!({
12534            "model": "served-alias",
12535            "messages": [{"role": "user", "content": "Call capture_quality_marker."}],
12536            "tools": [{
12537                "type": "function",
12538                "function": {
12539                    "name": "capture_quality_marker",
12540                    "description": "Record one marker.",
12541                    "parameters": {
12542                        "type": "object",
12543                        "properties": {
12544                            "marker": {"type": "string", "enum": ["ferrum0401"]},
12545                            "checksum": {"type": "string", "enum": ["S0004"]}
12546                        },
12547                        "required": ["marker", "checksum"]
12548                    }
12549                }
12550            }],
12551            "tool_choice": "required"
12552        }))
12553        .expect("tool request parses");
12554
12555        validate_chat_request(&request).expect("tool request validates");
12556        let internal = convert_chat_request(&request).expect("convert");
12557
12558        assert!(
12559            !internal.prompt.contains(
12560                "Output only a single JSON object containing the selected function arguments"
12561            ),
12562            "{}",
12563            internal.prompt
12564        );
12565        assert!(
12566            internal.prompt.contains("\"tool_choice\":\"required\""),
12567            "{}",
12568            internal.prompt
12569        );
12570        assert_eq!(internal.metadata["openai_tool_choice"], "required");
12571        match internal.sampling_params.response_format {
12572            ferrum_types::ResponseFormat::JsonSchema(ref schema) => {
12573                assert!(schema.contains(r#""enum":["ferrum0401"]"#), "{schema}");
12574                assert!(schema.contains(r#""enum":["S0004"]"#), "{schema}");
12575            }
12576            ref other => panic!("expected forced tool json schema, got {other:?}"),
12577        }
12578    }
12579
12580    #[test]
12581    fn required_tool_choice_suppresses_conflicting_response_format_instruction() {
12582        let request: ChatCompletionsRequest =
12583            serde_json::from_value(required_tool_with_strict_response_format_request(false))
12584                .expect("request parses");
12585
12586        validate_chat_request(&request).expect("request validates");
12587        let internal = convert_chat_request(&request).expect("convert");
12588
12589        assert!(
12590            !internal.prompt.contains("response_format requires"),
12591            "required tool output must not receive a conflicting content-schema instruction: {}",
12592            internal.prompt
12593        );
12594        let ferrum_types::ResponseFormat::JsonSchema(schema) =
12595            internal.sampling_params.response_format
12596        else {
12597            panic!("single required tool must use its argument schema");
12598        };
12599        let schema: Value = serde_json::from_str(&schema).expect("tool schema JSON");
12600        assert!(schema["properties"].get("city").is_some(), "{schema}");
12601        assert!(schema["properties"].get("answer").is_none(), "{schema}");
12602    }
12603
12604    #[test]
12605    fn required_multiple_tools_do_not_force_the_first_tool_schema() {
12606        let request: ChatCompletionsRequest = serde_json::from_value(json!({
12607            "model": "stub-model",
12608            "messages": [{"role": "user", "content": "Use the appropriate tool."}],
12609            "tools": [
12610                {
12611                    "type": "function",
12612                    "function": {
12613                        "name": "weather",
12614                        "parameters": {
12615                            "type": "object",
12616                            "properties": {"city": {"type": "string"}},
12617                            "required": ["city"]
12618                        }
12619                    }
12620                },
12621                {
12622                    "type": "function",
12623                    "function": {
12624                        "name": "calendar",
12625                        "parameters": {
12626                            "type": "object",
12627                            "properties": {"date": {"type": "string"}},
12628                            "required": ["date"]
12629                        }
12630                    }
12631                }
12632            ],
12633            "tool_choice": "required"
12634        }))
12635        .expect("request parses");
12636
12637        validate_chat_request(&request).expect("request validates");
12638        let internal = convert_chat_request(&request).expect("convert");
12639        assert_eq!(
12640            internal.sampling_params.response_format,
12641            ferrum_types::ResponseFormat::Text,
12642            "required permits either declared tool, so guided decoding cannot bind the first tool's arguments"
12643        );
12644    }
12645
12646    #[test]
12647    fn omitted_single_unrelated_tool_keeps_text_response_format() {
12648        let request: ChatCompletionsRequest = serde_json::from_value(json!({
12649            "model": "served-alias",
12650            "messages": [{"role": "user", "content": "讲一个短笑话。"}],
12651            "tools": [{
12652                "type": "function",
12653                "function": {
12654                    "name": "get_weather",
12655                    "description": "查询指定城市的当前天气",
12656                    "parameters": {
12657                        "type": "object",
12658                        "properties": {"city": {"type": "string"}},
12659                        "required": ["city"]
12660                    }
12661                }
12662            }]
12663        }))
12664        .expect("tool request parses");
12665
12666        validate_chat_request(&request).expect("tool request validates");
12667        let internal = convert_chat_request(&request).expect("convert");
12668        assert_eq!(
12669            internal.sampling_params.response_format,
12670            ferrum_types::ResponseFormat::Text
12671        );
12672    }
12673
12674    #[test]
12675    fn tool_choice_none_omits_tools_from_model_template_prompt() {
12676        let request: ChatCompletionsRequest = serde_json::from_value(json!({
12677            "model": "served-alias",
12678            "messages": [
12679                {"role": "user", "content": "Use the weather tool if needed."},
12680                {
12681                    "role": "assistant",
12682                    "content": null,
12683                    "tool_calls": [{
12684                        "id": "call_1",
12685                        "type": "function",
12686                        "function": {"name": "weather", "arguments": "{\"city\":\"Paris\"}"}
12687                    }]
12688                },
12689                {"role": "tool", "tool_call_id": "call_1", "content": "{\"temp\":22}"}
12690            ],
12691            "tools": [{
12692                "type": "function",
12693                "function": {"name": "weather", "parameters": {"type": "object"}}
12694            }],
12695            "tool_choice": "none"
12696        }))
12697        .expect("tool_choice none request parses");
12698        let template = ModelChatTemplate::new(
12699            "{% set tools_in_user_message = true %}{% if tools %}<tools>{% for tool in tools %}{{ tool.function.name }}{% endfor %}</tools>{% endif %}{% for message in messages %}[{{ message.role }}]{{ message.content }}{% endfor %}{% if add_generation_prompt %}[assistant]{% endif %}",
12700            "tool-choice-none-template",
12701        );
12702
12703        validate_chat_request(&request).expect("tool_choice none request validates");
12704        let internal = convert_chat_request_with_template_model(
12705            &request,
12706            "served-template-model",
12707            Some(&template),
12708        )
12709        .expect("convert");
12710        assert!(
12711            !internal.prompt.contains("<tools>"),
12712            "tool_choice none must not expose tools to the model template: {}",
12713            internal.prompt
12714        );
12715        assert!(internal.prompt.contains("[tool]"), "{}", internal.prompt);
12716        assert_eq!(
12717            internal.metadata["openai_tools"][0]["function"]["name"],
12718            "weather"
12719        );
12720        assert_eq!(internal.metadata["openai_tool_choice"], "none");
12721        let Some(ferrum_types::ApiRequest::Chat(api)) = internal.api_request.as_ref() else {
12722            panic!("expected structured chat api_request");
12723        };
12724        assert_eq!(api.tools[0].function.name, "weather");
12725        assert_eq!(
12726            api.tool_choice,
12727            Some(ferrum_types::ApiToolChoice::Mode("none".into()))
12728        );
12729    }
12730
12731    #[test]
12732    fn specific_tool_choice_parses_into_structured_api_request() {
12733        let request: ChatCompletionsRequest = serde_json::from_value(json!({
12734            "model": "qwen3",
12735            "messages": [{"role": "user", "content": "Use the selected tool."}],
12736            "tools": [
12737                {
12738                    "type": "function",
12739                    "function": {"name": "weather", "parameters": {"type": "object"}}
12740                },
12741                {
12742                    "type": "function",
12743                    "function": {"name": "calendar", "parameters": {"type": "object"}}
12744                }
12745            ],
12746            "tool_choice": {
12747                "type": "function",
12748                "function": {"name": "weather"}
12749            }
12750        }))
12751        .expect("specific tool_choice request parses");
12752
12753        validate_chat_request(&request).expect("specific tool_choice validates");
12754        let internal = convert_chat_request(&request).expect("convert");
12755        assert!(internal.prompt.contains("\"tool_choice\":{"));
12756        assert!(internal.prompt.contains("\"name\":\"weather\""));
12757        let Some(ferrum_types::ApiRequest::Chat(api)) = internal.api_request.as_ref() else {
12758            panic!("expected structured chat api_request");
12759        };
12760        assert_eq!(
12761            api.tool_choice,
12762            Some(ferrum_types::ApiToolChoice::Function {
12763                tool_type: "function".to_string(),
12764                function: ferrum_types::ApiToolChoiceFunction {
12765                    name: "weather".to_string()
12766                },
12767            })
12768        );
12769
12770        let invalid: ChatCompletionsRequest = serde_json::from_value(json!({
12771            "model": "qwen3",
12772            "messages": [{"role": "user", "content": "Use the selected tool."}],
12773            "tools": [{
12774                "type": "function",
12775                "function": {"name": "weather", "parameters": {"type": "object"}}
12776            }],
12777            "tool_choice": {
12778                "type": "function",
12779                "function": {"name": "calendar"}
12780            }
12781        }))
12782        .expect("invalid specific tool_choice request parses");
12783        let err = validate_chat_request(&invalid).expect_err("undeclared tool should reject");
12784        match err {
12785            ServerError::InvalidRequest { param, .. } => {
12786                assert_eq!(param.as_deref(), Some("tool_choice"));
12787            }
12788            other => panic!("expected invalid_request_error for tool_choice, got {other:?}"),
12789        }
12790    }
12791
12792    #[test]
12793    fn legacy_function_role_messages_parse_into_structured_api_request() {
12794        let request: ChatCompletionsRequest = serde_json::from_value(json!({
12795            "model": "mystery-model",
12796            "messages": [
12797                {"role": "user", "content": "Call weather."},
12798                {
12799                    "role": "assistant",
12800                    "content": null,
12801                    "function_call": {"name": "weather", "arguments": "{\"city\":\"Paris\"}"}
12802                },
12803                {"role": "function", "name": "weather", "content": "{\"forecast\":\"sunny\"}"}
12804            ],
12805            "functions": [{
12806                "name": "weather",
12807                "parameters": {
12808                    "type": "object",
12809                    "properties": {"city": {"type": "string"}},
12810                    "required": ["city"]
12811                }
12812            }],
12813            "function_call": "auto"
12814        }))
12815        .expect("legacy function request parses");
12816
12817        validate_chat_request(&request).expect("legacy function request validates");
12818        let internal = convert_chat_request(&request).expect("convert");
12819        assert!(
12820            internal
12821                .prompt
12822                .contains("<|function|>\n{\"forecast\":\"sunny\"}</s>"),
12823            "legacy function role should be preserved in fallback template: {}",
12824            internal.prompt
12825        );
12826        assert_eq!(
12827            internal.metadata["openai_legacy_functions"][0]["name"],
12828            "weather"
12829        );
12830        assert_eq!(internal.metadata["openai_legacy_function_call"], "auto");
12831        let Some(ferrum_types::ApiRequest::Chat(api)) = internal.api_request.as_ref() else {
12832            panic!("expected structured chat api_request");
12833        };
12834        assert_eq!(api.messages.len(), 3);
12835        assert_eq!(api.messages[2].role, ferrum_types::ApiMessageRole::Function);
12836        assert_eq!(api.messages[2].name.as_deref(), Some("weather"));
12837        assert_eq!(
12838            api.messages[1]
12839                .function_call
12840                .as_ref()
12841                .map(|call| call.name.as_str()),
12842            Some("weather")
12843        );
12844        assert_eq!(api.legacy_functions[0].name, "weather");
12845        assert_eq!(
12846            api.legacy_function_call,
12847            Some(ferrum_types::ApiFunctionCallChoice::Mode("auto".into()))
12848        );
12849    }
12850
12851    #[test]
12852    fn specific_legacy_function_call_parses_into_structured_api_request() {
12853        let request: ChatCompletionsRequest = serde_json::from_value(json!({
12854            "model": "mystery-model",
12855            "messages": [{"role": "user", "content": "Use the selected function."}],
12856            "functions": [
12857                {"name": "weather", "parameters": {"type": "object"}},
12858                {"name": "calendar", "parameters": {"type": "object"}}
12859            ],
12860            "function_call": {"name": "weather"}
12861        }))
12862        .expect("specific function_call request parses");
12863
12864        validate_chat_request(&request).expect("specific function_call validates");
12865        let internal = convert_chat_request(&request).expect("convert");
12866        assert!(internal.prompt.contains("\"function_call\":{"));
12867        assert!(internal.prompt.contains("\"name\":\"weather\""));
12868        let Some(ferrum_types::ApiRequest::Chat(api)) = internal.api_request.as_ref() else {
12869            panic!("expected structured chat api_request");
12870        };
12871        assert_eq!(
12872            api.legacy_function_call,
12873            Some(ferrum_types::ApiFunctionCallChoice::Function {
12874                name: "weather".to_string(),
12875            })
12876        );
12877
12878        let invalid: ChatCompletionsRequest = serde_json::from_value(json!({
12879            "model": "mystery-model",
12880            "messages": [{"role": "user", "content": "Use the selected function."}],
12881            "functions": [{"name": "weather", "parameters": {"type": "object"}}],
12882            "function_call": {"name": "calendar"}
12883        }))
12884        .expect("invalid specific function_call request parses");
12885        let err = validate_chat_request(&invalid).expect_err("undeclared function should reject");
12886        match err {
12887            ServerError::InvalidRequest { param, .. } => {
12888                assert_eq!(param.as_deref(), Some("function_call"));
12889            }
12890            other => panic!("expected invalid_request_error for function_call, got {other:?}"),
12891        }
12892    }
12893
12894    #[test]
12895    fn stream_text_delta_handles_unicode_boundaries() {
12896        let mut sent_len = 0usize;
12897        assert_eq!(stream_text_delta("你好", &mut sent_len), "你好");
12898        assert_eq!(sent_len, "你好".len());
12899        assert_eq!(stream_text_delta("你好世界", &mut sent_len), "世界");
12900        assert_eq!(sent_len, "你好世界".len());
12901    }
12902
12903    #[test]
12904    fn stream_text_delta_recovers_from_non_boundary_offset() {
12905        let mut sent_len = 1usize;
12906        assert_eq!(stream_text_delta("你好", &mut sent_len), "");
12907        assert_eq!(sent_len, "你好".len());
12908    }
12909
12910    #[test]
12911    fn assistant_tool_call_serializes_openai_shape() {
12912        let message = ChatMessage {
12913            role: MessageRole::Assistant,
12914            content: String::new(),
12915            reasoning: None,
12916            name: None,
12917            tool_calls: Some(vec![ChatToolCall {
12918                index: None,
12919                id: "call_1".to_string(),
12920                tool_type: "function".to_string(),
12921                function: ChatFunctionCall {
12922                    name: "weather".to_string(),
12923                    arguments: "{\"city\":\"Paris\"}".to_string(),
12924                },
12925            }]),
12926            tool_call_id: None,
12927            function_call: None,
12928        };
12929        let value = serde_json::to_value(message).expect("serialize");
12930        assert_eq!(value["role"], "assistant");
12931        assert_eq!(value["tool_calls"][0]["type"], "function");
12932        assert_eq!(value["tool_calls"][0]["function"]["name"], "weather");
12933    }
12934
12935    #[test]
12936    fn unsupported_multimodal_content_is_not_silently_dropped() {
12937        let err = serde_json::from_value::<ChatCompletionsRequest>(json!({
12938            "model": "stub-model",
12939            "messages": [{
12940                "role": "user",
12941                "content": [
12942                    {"type": "text", "text": "describe this"},
12943                    {"type": "image_url", "image_url": {"url": "data:image/png;base64,AA=="}}
12944                ]
12945            }]
12946        }))
12947        .expect_err("unsupported content part should fail parsing");
12948        assert!(
12949            err.to_string()
12950                .contains("unsupported message content part type"),
12951            "unexpected error: {err}"
12952        );
12953    }
12954
12955    #[tokio::test]
12956    async fn completions_endpoint_uses_stub_engine() {
12957        let request = CompletionsRequest {
12958            model: "stub-model".to_string(),
12959            prompt: CompletionPrompt::Text("complete me".to_string()),
12960            max_tokens: Some(8),
12961            temperature: Some(0.0),
12962            top_p: None,
12963            n: None,
12964            stream: None,
12965            stop: None,
12966            logprobs: None,
12967            logit_bias: None,
12968        };
12969        let response = completions_handler(State(state_with_stub("done")), Ok(Json(request)))
12970            .await
12971            .expect("completion response");
12972        assert_eq!(response.status(), AxumStatusCode::OK);
12973        let body = response_json(response).await;
12974        assert_eq!(body["object"], "text_completion");
12975        assert_eq!(body["choices"][0]["text"], "done");
12976        assert_eq!(body["usage"]["prompt_tokens"], 7);
12977        assert_eq!(body["usage"]["completion_tokens"], 2);
12978    }
12979
12980    #[tokio::test]
12981    async fn route_completions_rejects_non_string_prompt_with_field_param() {
12982        for prompt in [
12983            json!(["a", "b"]),
12984            json!({"text": "complete me"}),
12985            Value::Null,
12986        ] {
12987            let response = post_json(
12988                router_with_stub("unused"),
12989                "/v1/completions",
12990                json!({
12991                    "model": "stub-model",
12992                    "prompt": prompt
12993                }),
12994            )
12995            .await;
12996            assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
12997            let body = response_json(response).await;
12998            assert_eq!(body["error"]["type"], "invalid_request_error");
12999            assert_eq!(body["error"]["param"], "prompt");
13000        }
13001
13002        let response = post_json(
13003            router_with_stub("unused"),
13004            "/v1/completions",
13005            json!({"model": "stub-model"}),
13006        )
13007        .await;
13008        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
13009        let body = response_json(response).await;
13010        assert_eq!(body["error"]["type"], "invalid_request_error");
13011        assert_eq!(body["error"]["param"], "prompt");
13012    }
13013
13014    #[tokio::test]
13015    async fn stream_options_without_stream_is_invalid() {
13016        let request = chat_request(json!({"stream_options": {"include_usage": true}}));
13017        let err = chat_completions_handler(
13018            State(state_with_stub("unused")),
13019            HeaderMap::new(),
13020            Ok(Json(request)),
13021        )
13022        .await
13023        .expect_err("stream_options without stream should reject");
13024        let (status, body) = error_json(err).await;
13025        assert_eq!(status, AxumStatusCode::BAD_REQUEST);
13026        assert_eq!(body["error"]["param"], "stream_options");
13027        assert_eq!(body["error"]["type"], "invalid_request_error");
13028    }
13029
13030    #[tokio::test]
13031    async fn unknown_stream_option_is_rejected_instead_of_ignored() {
13032        let response = post_json(
13033            router_with_stub("unused"),
13034            "/v1/chat/completions",
13035            json!({
13036                "model": "stub-model",
13037                "messages": [{"role": "user", "content": "hello"}],
13038                "stream": true,
13039                "stream_options": {"continuous_usage_stats": true}
13040            }),
13041        )
13042        .await;
13043
13044        assert_eq!(response.status(), AxumStatusCode::BAD_REQUEST);
13045        let body = response_json(response).await;
13046        assert_eq!(body["error"]["type"], "invalid_request_error");
13047        assert!(
13048            body["error"]["message"]
13049                .as_str()
13050                .unwrap_or_default()
13051                .contains("invalid chat completions request"),
13052            "body: {body}"
13053        );
13054    }
13055
13056    #[tokio::test]
13057    async fn json_object_rejects_markdown_fence_instead_of_repairing() {
13058        let request = chat_request(json!({
13059            "response_format": {"type": "json_object"}
13060        }));
13061        let err = chat_completions_handler(
13062            State(state_with_stub("```json\n{\"answer\":\"yes\"}\n```")),
13063            HeaderMap::new(),
13064            Ok(Json(request)),
13065        )
13066        .await
13067        .expect_err("fenced json_object must fail");
13068        let (status, body) = error_json(err).await;
13069        assert_eq!(status, AxumStatusCode::INTERNAL_SERVER_ERROR);
13070        assert_eq!(body["error"]["type"], "internal_server_error");
13071        assert!(body["error"]["message"]
13072            .as_str()
13073            .unwrap_or_default()
13074            .contains("response_format.json_object: invalid JSON"));
13075    }
13076
13077    #[tokio::test]
13078    async fn streaming_json_object_buffers_thinking_and_emits_clean_json_content() {
13079        let response = post_json(
13080            router_with_stub_stream_chunks(&[
13081                "<think>\n好的,我需要输出 JSON。",
13082                "\n</think>\n\n",
13083                "{\"name\":\"李四\",\"age\":30}",
13084            ]),
13085            "/v1/chat/completions",
13086            json!({
13087                "model": "stub-model",
13088                "messages": [{"role": "user", "content": "输出JSON(name,age):李四,30岁"}],
13089                "stream": true,
13090                "response_format": {"type": "json_object"}
13091            }),
13092        )
13093        .await;
13094        assert_eq!(response.status(), AxumStatusCode::OK);
13095        let body = response_text(response).await;
13096        assert!(body.contains("data: [DONE]"), "missing DONE: {body}");
13097        assert!(
13098            body.contains(r#""content":"{\"name\":\"李四\",\"age\":30}""#),
13099            "stream should emit clean JSON content: {body}"
13100        );
13101        assert!(
13102            body.contains(r#""reasoning":"\n好的,我需要输出 JSON。\n""#),
13103            "stream should keep thinking in reasoning field: {body}"
13104        );
13105        assert!(
13106            !body.contains(r#""content":"<think"#)
13107                && !body.contains(r#""content":"好的"#)
13108                && !body.contains(r#""content":"我需要"#),
13109            "thinking text must not leak as streamed content: {body}"
13110        );
13111    }
13112
13113    fn prompt_opened_literal_json_template() -> ModelChatTemplate {
13114        let template = ModelChatTemplate::new(
13115            "{% for message in messages %}[{{ message.role }}]{{ message.content }}{% endfor %}{% if add_generation_prompt %}<assistant><think>{% endif %}",
13116            "prompt-opened-text-test",
13117        );
13118        assert_eq!(template.output_protocol, ModelOutputProtocol::Text);
13119        assert_eq!(
13120            template.reasoning_protocol,
13121            ModelReasoningProtocol::PromptOpened
13122        );
13123        let request = chat_request(json!({"response_format": {"type": "json_object"}}));
13124        let internal =
13125            convert_chat_request_with_template_model(&request, "stub-model", Some(&template))
13126                .expect("convert prompt-opened Text request");
13127        assert!(internal.prompt.ends_with("<think>"));
13128        template
13129    }
13130
13131    #[tokio::test]
13132    async fn json_object_preserves_literal_think_tags_after_prompt_opened_reasoning_sync() {
13133        let response = post_json(
13134            router_with_stub_and_template(
13135                "reason</think>\n{\"text\":\"<think>literal</think>\"}",
13136                prompt_opened_literal_json_template(),
13137            ),
13138            "/v1/chat/completions",
13139            json!({
13140                "model": "stub-model",
13141                "messages": [{"role": "user", "content": "Return a JSON object."}],
13142                "response_format": {"type": "json_object"}
13143            }),
13144        )
13145        .await;
13146        let status = response.status();
13147        let body = response_json(response).await;
13148        assert_eq!(status, AxumStatusCode::OK, "{body}");
13149        assert!(body.get("error").is_none(), "{body}");
13150        let message = &body["choices"][0]["message"];
13151        assert_eq!(message["content"], r#"{"text":"<think>literal</think>"}"#);
13152        assert_eq!(message["reasoning"], "reason");
13153        assert_eq!(body["choices"][0]["finish_reason"], "stop");
13154    }
13155
13156    #[tokio::test]
13157    async fn json_object_preserves_literal_think_tags_after_prompt_opened_reasoning_sse() {
13158        for chunks in [
13159            vec!["reason</think>\n{\"text\":\"<think>literal</think>\"}"],
13160            vec![
13161                "reason</thi",
13162                "nk>\n{\"text\":\"<thi",
13163                "nk>literal</thi",
13164                "nk>\"}",
13165            ],
13166        ] {
13167            let router = AxumServer::from_llm(Arc::new(StubLlm::with_stream_chunks(&chunks)))
13168                .with_prompt_template(Some(prompt_opened_literal_json_template()))
13169                .build_router();
13170            let response = post_json(
13171                router,
13172                "/v1/chat/completions",
13173                json!({
13174                    "model": "stub-model",
13175                    "messages": [{"role": "user", "content": "Return a JSON object."}],
13176                    "stream": true,
13177                    "stream_options": {"include_usage": true},
13178                    "response_format": {"type": "json_object"}
13179                }),
13180            )
13181            .await;
13182            let status = response.status();
13183            let body = response_text(response).await;
13184            assert_eq!(status, AxumStatusCode::OK, "{body}");
13185            let normalized = body.replace("\r\n", "\n");
13186            assert_eq!(normalized.matches("data: [DONE]").count(), 1, "{body}");
13187            assert!(normalized.ends_with("data: [DONE]\n\n"), "{body}");
13188            let events = responses_sse_json_events(&body);
13189            assert!(
13190                events.iter().all(|event| event.get("error").is_none()),
13191                "{body}"
13192            );
13193            let content: String = events
13194                .iter()
13195                .filter_map(|event| event["choices"][0]["delta"]["content"].as_str())
13196                .collect();
13197            let reasoning: String = events
13198                .iter()
13199                .filter_map(|event| event["choices"][0]["delta"]["reasoning"].as_str())
13200                .collect();
13201            assert_eq!(content, r#"{"text":"<think>literal</think>"}"#);
13202            assert_eq!(reasoning, "reason");
13203            assert_eq!(
13204                serde_json::from_str::<Value>(&content).expect("intact JSON body"),
13205                json!({"text": "<think>literal</think>"})
13206            );
13207            let terminals: Vec<_> = events
13208                .iter()
13209                .enumerate()
13210                .filter(|(_, event)| !event["choices"][0]["finish_reason"].is_null())
13211                .collect();
13212            assert_eq!(terminals.len(), 1, "{body}");
13213            let (terminal_index, terminal) = terminals[0];
13214            assert_eq!(terminal["choices"][0]["finish_reason"], "stop");
13215            for event in &events[terminal_index..] {
13216                for field in ["content", "reasoning", "reasoning_content"] {
13217                    assert!(
13218                        event["choices"][0]["delta"][field]
13219                            .as_str()
13220                            .unwrap_or_default()
13221                            .is_empty(),
13222                        "payload after terminal: {event}"
13223                    );
13224                }
13225            }
13226            let usages: Vec<_> = events
13227                .iter()
13228                .enumerate()
13229                .filter(|(_, event)| !event["usage"].is_null())
13230                .collect();
13231            assert_eq!(usages.len(), 1, "{body}");
13232            let (usage_index, usage) = usages[0];
13233            assert!(terminal_index < usage_index, "{body}");
13234            assert_eq!(usage_index, events.len() - 1, "usage must be last: {body}");
13235            assert_eq!(usage["choices"], json!([]));
13236        }
13237    }
13238
13239    #[tokio::test]
13240    async fn json_object_rejects_non_json_model_output() {
13241        let request = chat_request(json!({
13242            "response_format": {"type": "json_object"}
13243        }));
13244        let err = chat_completions_handler(
13245            State(state_with_stub("not json")),
13246            HeaderMap::new(),
13247            Ok(Json(request)),
13248        )
13249        .await
13250        .expect_err("invalid json_object must fail");
13251        let (status, body) = error_json(err).await;
13252        assert_eq!(status, AxumStatusCode::INTERNAL_SERVER_ERROR);
13253        assert_eq!(body["error"]["type"], "internal_server_error");
13254        assert!(body["error"]["message"]
13255            .as_str()
13256            .unwrap_or_default()
13257            .contains("response_format.json_object"));
13258    }
13259
13260    #[test]
13261    fn one_of_strict_json_schema_reaches_hard_decoder() {
13262        let request = chat_request(json!({
13263            "response_format": {
13264                "type": "json_schema",
13265                "json_schema": {
13266                    "name": "unsupported",
13267                    "strict": true,
13268                    "schema": {"oneOf": [{"type": "string"}, {"type": "integer"}]}
13269                }
13270            }
13271        }));
13272        validate_chat_request(&request).expect("oneOf strict schema should validate");
13273        let internal = convert_chat_request(&request).expect("convert oneOf strict schema");
13274        let ferrum_types::ResponseFormat::JsonSchema(schema) =
13275            internal.sampling_params.response_format
13276        else {
13277            panic!("strict schema did not reach hard decoder");
13278        };
13279        assert_eq!(
13280            serde_json::from_str::<serde_json::Value>(&schema).unwrap()["oneOf"],
13281            json!([{"type": "string"}, {"type": "integer"}])
13282        );
13283        let schema = serde_json::from_str::<serde_json::Value>(&schema).unwrap();
13284        validate_json_text_against_schema(&schema, r#""answer""#)
13285            .expect("oneOf string branch should pass final validation");
13286        validate_json_text_against_schema(&schema, "7")
13287            .expect("oneOf integer branch should pass final validation");
13288        assert!(validate_json_text_against_schema(&schema, "true").is_err());
13289    }
13290
13291    #[tokio::test]
13292    async fn missing_json_schema_schema_rejects_with_field_param() {
13293        let request = chat_request(json!({
13294            "response_format": {
13295                "type": "json_schema",
13296                "json_schema": {
13297                    "name": "missing_schema",
13298                    "strict": true
13299                }
13300            }
13301        }));
13302        let err = chat_completions_handler(
13303            State(state_with_stub("unused")),
13304            HeaderMap::new(),
13305            Ok(Json(request)),
13306        )
13307        .await
13308        .expect_err("missing strict schema should reject");
13309        let (status, body) = error_json(err).await;
13310        assert_eq!(status, AxumStatusCode::BAD_REQUEST);
13311        assert_eq!(body["error"]["param"], "response_format.json_schema");
13312        assert_eq!(body["error"]["type"], "invalid_request_error");
13313        assert!(body["error"]["message"]
13314            .as_str()
13315            .unwrap()
13316            .contains("schema is required"));
13317    }
13318
13319    #[test]
13320    fn non_strict_json_schema_is_preserved_but_not_hard_masked() {
13321        let request = chat_request(json!({
13322            "response_format": {
13323                "type": "json_schema",
13324                "json_schema": {
13325                    "name": "best_effort",
13326                    "strict": false,
13327                    "schema": {"oneOf": [{"type": "string"}, {"type": "integer"}]}
13328                }
13329            }
13330        }));
13331
13332        validate_chat_request(&request).expect("non-strict schema should not boundary reject");
13333        let internal = convert_chat_request(&request).expect("convert non-strict schema");
13334        assert!(
13335            internal
13336                .prompt
13337                .contains("response_format requires a single valid JSON value"),
13338            "response_format instruction should reach the model prompt: {}",
13339            internal.prompt
13340        );
13341        assert!(
13342            internal.prompt.contains("\"oneOf\""),
13343            "schema should reach the model prompt: {}",
13344            internal.prompt
13345        );
13346        assert_eq!(
13347            internal.sampling_params.response_format,
13348            ferrum_types::ResponseFormat::Text,
13349            "non-strict json_schema must stay best-effort instead of enabling hard guided decode"
13350        );
13351        let Some(ferrum_types::ApiRequest::Chat(api)) = internal.api_request.as_ref() else {
13352            panic!("expected structured chat api_request");
13353        };
13354        assert_eq!(
13355            api.response_format
13356                .as_ref()
13357                .and_then(|format| format.json_schema.as_ref())
13358                .and_then(|schema| schema.strict),
13359            Some(false)
13360        );
13361    }
13362
13363    #[test]
13364    fn json_object_response_format_instruction_reaches_model_prompt() {
13365        let request = chat_request(json!({
13366            "response_format": {"type": "json_object"}
13367        }));
13368
13369        let internal = convert_chat_request(&request).expect("convert json_object");
13370        assert!(
13371            internal
13372                .prompt
13373                .contains("response_format requires a single valid JSON object"),
13374            "response_format instruction should reach the model prompt: {}",
13375            internal.prompt
13376        );
13377        assert!(
13378            internal.prompt.contains("Output only JSON"),
13379            "JSON-only instruction should reach the model prompt: {}",
13380            internal.prompt
13381        );
13382        assert_eq!(
13383            internal.sampling_params.response_format,
13384            ferrum_types::ResponseFormat::JsonObject,
13385            "json_object must reach the tokenizer-aware hard decoder"
13386        );
13387        assert_eq!(
13388            internal.sampling_params.structured_output_start,
13389            StructuredOutputStart::Immediate
13390        );
13391    }
13392
13393    fn harmony_json_template() -> ModelChatTemplate {
13394        let mut template = ModelChatTemplate::new(
13395            "{% for message in messages %}<|start|>{{ message.role }}<|message|>{{ message.content }}<|end|>{% endfor %}{% if add_generation_prompt %}<|start|>assistant{% endif %}",
13396            "harmony-structured-template",
13397        );
13398        template.output_protocol = ModelOutputProtocol::HarmonyGptOss;
13399        template
13400    }
13401
13402    #[test]
13403    fn harmony_structured_format_activates_at_final_payload() {
13404        let template = harmony_json_template();
13405        for (response_format, constrained) in [
13406            (json!({"type": "json_object"}), true),
13407            (
13408                json!({
13409                    "type": "json_schema",
13410                    "json_schema": {
13411                        "name": "answer",
13412                        "strict": true,
13413                        "schema": {
13414                            "type": "object",
13415                            "properties": {"answer": {"type": "integer"}},
13416                            "required": ["answer"],
13417                            "additionalProperties": false
13418                        }
13419                    }
13420                }),
13421                true,
13422            ),
13423            (
13424                json!({
13425                    "type": "json_schema",
13426                    "json_schema": {
13427                        "name": "best_effort",
13428                        "strict": false,
13429                        "schema": {"type": "object"}
13430                    }
13431                }),
13432                false,
13433            ),
13434            (json!({"type": "text"}), false),
13435        ] {
13436            let request = chat_request(json!({"response_format": response_format}));
13437            let internal =
13438                convert_chat_request_with_template_model(&request, "stub-model", Some(&template))
13439                    .unwrap();
13440            assert_eq!(
13441                internal.sampling_params.structured_output_start,
13442                if constrained {
13443                    StructuredOutputStart::HarmonyFinal
13444                } else {
13445                    StructuredOutputStart::Immediate
13446                }
13447            );
13448            assert_eq!(
13449                internal.sampling_params.response_completion_boundary,
13450                ResponseCompletionBoundary::Immediate,
13451                "Harmony framing must not be gated on a Text reasoning delimiter"
13452            );
13453            internal.sampling_params.validate().unwrap();
13454        }
13455    }
13456
13457    fn harmony_json_request(stream: bool) -> Value {
13458        json!({
13459            "model": "stub-model",
13460            "messages": [{"role": "user", "content": "Return an answer object."}],
13461            "stream": stream,
13462            "response_format": {
13463                "type": "json_schema",
13464                "json_schema": {
13465                    "name": "answer",
13466                    "strict": true,
13467                    "schema": {
13468                        "type": "object",
13469                        "properties": {"answer": {"type": "integer"}},
13470                        "required": ["answer"],
13471                        "additionalProperties": false
13472                    }
13473                }
13474            }
13475        })
13476    }
13477
13478    #[tokio::test]
13479    async fn harmony_strict_json_routes_validate_final_payload_in_sync_and_sse() {
13480        for (chunks, finish_reason, reasoning) in [
13481            (
13482                vec![
13483                    "<|channel|>fi",
13484                    "nal<|message|>{\"answer\":",
13485                    "42}<|return|>",
13486                ],
13487                FinishReason::EOS,
13488                "",
13489            ),
13490            (
13491                vec![
13492                    "<|channel|>analysis<|message|>Compute.",
13493                    "<|end|><|start|>assistant<|channel|>fi",
13494                    "nal<|message|>{\"answer\":42}<|return|>",
13495                ],
13496                FinishReason::EOS,
13497                "Compute.",
13498            ),
13499            (
13500                vec!["<|channel|>final<|message|>{\"answer\":", "42}"],
13501                FinishReason::Length,
13502                "",
13503            ),
13504            (
13505                vec!["<|channel|>final<|message|>{\"answer\":", "42}"],
13506                FinishReason::Stop,
13507                "",
13508            ),
13509        ] {
13510            for stream in [false, true] {
13511                let engine = StubLlm {
13512                    finish_reason,
13513                    ..StubLlm::with_stream_chunks(&chunks)
13514                };
13515                let router = AxumServer::from_llm(Arc::new(engine))
13516                    .with_prompt_template(Some(harmony_json_template()))
13517                    .build_router();
13518                let mut request = harmony_json_request(stream);
13519                if finish_reason == FinishReason::Stop {
13520                    // An explicit stop at the model's terminal removes that
13521                    // marker while preserving the already complete JSON value.
13522                    request["stop"] = json!(["<|return|>"]);
13523                }
13524                let response = post_json(router, "/v1/chat/completions", request).await;
13525                assert_eq!(response.status(), AxumStatusCode::OK);
13526                if stream {
13527                    let body = response_text(response).await;
13528                    assert!(body.contains("data: [DONE]"));
13529                    let events = responses_sse_json_events(&body);
13530                    assert!(events.iter().all(|event| event.get("error").is_none()));
13531                    let content: String = events
13532                        .iter()
13533                        .filter_map(|event| event["choices"][0]["delta"]["content"].as_str())
13534                        .collect();
13535                    let actual_reasoning: String = events
13536                        .iter()
13537                        .filter_map(|event| event["choices"][0]["delta"]["reasoning"].as_str())
13538                        .collect();
13539                    assert_eq!(
13540                        serde_json::from_str::<Value>(&content).unwrap(),
13541                        json!({"answer": 42})
13542                    );
13543                    assert_eq!(actual_reasoning, reasoning);
13544                } else {
13545                    let body = response_json(response).await;
13546                    let message = &body["choices"][0]["message"];
13547                    assert_eq!(message["content"], "{\"answer\":42}");
13548                    assert_eq!(message["reasoning"].as_str().unwrap_or(""), reasoning);
13549                }
13550            }
13551        }
13552    }
13553
13554    #[tokio::test]
13555    async fn harmony_strict_json_routes_reject_bad_framing_and_payload_without_sse_leaks() {
13556        for output in [
13557            "{\"answer\":42}",
13558            "<|channel|>final<|message|>{\"answer\":42}",
13559            "<|channel|>final<|message|>{\"answer\":42}<|call|>",
13560            "<|channel|>analysis<|message|>Compute.<|end|>\
13561             <|start|>assistant<|channel|>final<|message|>{\"answer\":\"wrong\"}<|return|>",
13562        ] {
13563            for stream in [false, true] {
13564                let response = post_json(
13565                    router_with_stub_and_template(output, harmony_json_template()),
13566                    "/v1/chat/completions",
13567                    harmony_json_request(stream),
13568                )
13569                .await;
13570                if stream {
13571                    assert_eq!(response.status(), AxumStatusCode::OK);
13572                    let body = response_text(response).await;
13573                    assert!(body.contains("data: [DONE]"));
13574                    let events = responses_sse_json_events(&body);
13575                    assert!(events.iter().any(|event| event.get("error").is_some()));
13576                    for event in events {
13577                        for field in ["content", "reasoning"] {
13578                            assert!(event["choices"][0]["delta"][field]
13579                                .as_str()
13580                                .unwrap_or("")
13581                                .is_empty());
13582                        }
13583                    }
13584                } else {
13585                    assert_eq!(response.status(), AxumStatusCode::INTERNAL_SERVER_ERROR);
13586                    let body = response_json(response).await;
13587                    assert_eq!(body["error"]["type"], "internal_server_error");
13588                    assert!(body.get("choices").is_none());
13589                }
13590            }
13591        }
13592    }
13593
13594    #[test]
13595    fn json_object_thinking_template_activates_after_typed_end_delimiter() {
13596        let request = chat_request(json!({
13597            "response_format": {"type": "json_object"}
13598        }));
13599        let template = ModelChatTemplate::new(
13600            "{% for message in messages %}[{{ message.role }}]{{ message.content }}{% endfor %}{% if add_generation_prompt %}<assistant><think>\n{% endif %}",
13601            "thinking-test-template",
13602        );
13603
13604        assert_eq!(
13605            template.reasoning_protocol,
13606            ModelReasoningProtocol::PromptOpened
13607        );
13608        let internal =
13609            convert_chat_request_with_template_model(&request, "stub-model", Some(&template))
13610                .expect("convert thinking json_object");
13611
13612        assert!(internal.prompt.ends_with("<assistant><think>\n"));
13613        assert!(internal
13614            .prompt
13615            .contains("Output only JSON, with no markdown fences, no explanation, no chain-of-thought, and no extra text"));
13616        assert!(
13617            !internal.prompt.contains(THINK_END_TAG),
13618            "the instruction must not echo the typed end delimiter: {}",
13619            internal.prompt
13620        );
13621        assert_eq!(
13622            internal.sampling_params.structured_output_start,
13623            StructuredOutputStart::AfterDelimiter(THINK_END_TAG.to_string())
13624        );
13625        assert_eq!(
13626            internal.sampling_params.response_completion_boundary,
13627            ResponseCompletionBoundary::AfterDelimiterAndPayload {
13628                delimiter: THINK_END_TAG.to_string(),
13629                alternate_envelope: None,
13630            }
13631        );
13632    }
13633
13634    #[test]
13635    fn json_object_model_generated_thinking_activates_after_typed_end_delimiter() {
13636        let request = chat_request(json!({
13637            "response_format": {"type": "json_object"},
13638            "chat_template_kwargs": {"enable_thinking": true}
13639        }));
13640        let template = ModelChatTemplate::new(
13641            "{% for message in messages %}[{{ message.role }}]{{ message.content }}{% endfor %}{% if add_generation_prompt %}<assistant>{% if enable_thinking is defined and enable_thinking is false %}<think>\n\n</think>\n\n{% endif %}{% endif %}",
13642            "qwen3-model-generated-thinking-template",
13643        );
13644
13645        let internal =
13646            convert_chat_request_with_template_model(&request, "stub-model", Some(&template))
13647                .expect("convert model-generated thinking json_object");
13648
13649        assert!(!has_unclosed_thinking_block(&internal.prompt));
13650        assert!(internal.prompt.ends_with("<assistant>"));
13651        assert!(internal
13652            .prompt
13653            .contains("Output only JSON, with no markdown fences, no explanation, no chain-of-thought, and no extra text"));
13654        assert!(
13655            !internal.prompt.contains(THINK_START_TAG) && !internal.prompt.contains(THINK_END_TAG),
13656            "the instruction must not teach the model the typed reasoning delimiter: {}",
13657            internal.prompt
13658        );
13659        assert_eq!(
13660            internal.sampling_params.structured_output_start,
13661            StructuredOutputStart::AfterDelimiter(THINK_END_TAG.to_string())
13662        );
13663        assert_eq!(
13664            internal.sampling_params.response_completion_boundary,
13665            ResponseCompletionBoundary::AfterDelimiterAndPayload {
13666                delimiter: THINK_END_TAG.to_string(),
13667                alternate_envelope: None,
13668            }
13669        );
13670    }
13671
13672    #[test]
13673    fn strict_schema_model_generated_thinking_does_not_echo_typed_delimiter() {
13674        let request = chat_request(json!({
13675            "response_format": {
13676                "type": "json_schema",
13677                "json_schema": {
13678                    "name": "reasoning_result",
13679                    "strict": true,
13680                    "schema": {
13681                        "type": "object",
13682                        "properties": {
13683                            "result": {"type": "string", "const": "G00-c21-schema-OK"}
13684                        },
13685                        "required": ["result"],
13686                        "additionalProperties": false
13687                    }
13688                }
13689            },
13690            "chat_template_kwargs": {"enable_thinking": true}
13691        }));
13692        let template = ModelChatTemplate::new(
13693            "{% for message in messages %}[{{ message.role }}]{{ message.content }}{% endfor %}{% if add_generation_prompt %}<assistant>{% if enable_thinking is defined and enable_thinking is false %}<think>\n\n</think>\n\n{% endif %}{% endif %}",
13694            "qwen3-model-generated-thinking-template",
13695        );
13696
13697        let internal =
13698            convert_chat_request_with_template_model(&request, "stub-model", Some(&template))
13699                .expect("convert model-generated thinking strict schema");
13700
13701        assert!(!has_unclosed_thinking_block(&internal.prompt));
13702        assert!(internal.prompt.ends_with("<assistant>"));
13703        assert!(internal.prompt.contains("G00-c21-schema-OK"));
13704        assert!(
13705            !internal.prompt.contains(THINK_START_TAG) && !internal.prompt.contains(THINK_END_TAG),
13706            "the instruction must not teach the model the typed reasoning delimiter: {}",
13707            internal.prompt
13708        );
13709        assert_eq!(
13710            internal.sampling_params.structured_output_start,
13711            StructuredOutputStart::AfterDelimiter(THINK_END_TAG.to_string())
13712        );
13713        assert_eq!(
13714            internal.sampling_params.response_completion_boundary,
13715            ResponseCompletionBoundary::AfterDelimiterAndPayload {
13716                delimiter: THINK_END_TAG.to_string(),
13717                alternate_envelope: None,
13718            }
13719        );
13720    }
13721
13722    #[test]
13723    fn json_object_model_generated_thinking_hard_off_starts_immediately() {
13724        let request = chat_request(json!({
13725            "response_format": {"type": "json_object"},
13726            "chat_template_kwargs": {"enable_thinking": false}
13727        }));
13728        let template = ModelChatTemplate::new(
13729            "{% for message in messages %}[{{ message.role }}]{{ message.content }}{% endfor %}{% if add_generation_prompt %}<assistant>{% if enable_thinking is defined and enable_thinking is false %}<think>\n\n</think>\n\n{% endif %}{% endif %}",
13730            "qwen3-model-generated-thinking-template",
13731        );
13732
13733        let internal =
13734            convert_chat_request_with_template_model(&request, "stub-model", Some(&template))
13735                .expect("convert disabled model-generated thinking json_object");
13736
13737        assert_eq!(
13738            internal.sampling_params.structured_output_start,
13739            StructuredOutputStart::Immediate
13740        );
13741        assert_eq!(
13742            internal.sampling_params.response_completion_boundary,
13743            ResponseCompletionBoundary::Immediate
13744        );
13745        assert!(internal.prompt.contains("no chain-of-thought"));
13746    }
13747
13748    #[test]
13749    fn response_completion_contract_is_set_on_text_thinking_template() {
13750        let request = chat_request(json!({}));
13751        let template = ModelChatTemplate::new(
13752            "{% if add_generation_prompt %}<assistant><think>\n{% endif %}",
13753            "thinking-test-template",
13754        );
13755
13756        let internal =
13757            convert_chat_request_with_template_model(&request, "stub-model", Some(&template))
13758                .expect("convert thinking text request");
13759
13760        assert_eq!(
13761            internal.sampling_params.structured_output_start,
13762            StructuredOutputStart::Immediate
13763        );
13764        assert_eq!(
13765            internal.sampling_params.response_completion_boundary,
13766            ResponseCompletionBoundary::AfterDelimiterAndPayload {
13767                delimiter: THINK_END_TAG.to_string(),
13768                alternate_envelope: None,
13769            }
13770        );
13771    }
13772
13773    #[test]
13774    fn thinking_tool_request_compiles_typed_envelope_into_completion_contract() {
13775        let request = chat_request(json!({
13776            "tools": [{
13777                "type": "function",
13778                "function": {
13779                    "name": "weather",
13780                    "parameters": {
13781                        "type": "object",
13782                        "properties": {"city": {"type": "string"}},
13783                        "required": ["city"]
13784                    }
13785                }
13786            }]
13787        }));
13788        let template = ModelChatTemplate::new(
13789            "{% if tools %}<tool_call><function=name><parameter=key>value</parameter></function></tool_call>{% endif %}{% if add_generation_prompt %}<assistant><think>\n{% endif %}",
13790            "thinking-tool-template",
13791        );
13792
13793        let internal =
13794            convert_chat_request_with_template_model(&request, "stub-model", Some(&template))
13795                .expect("convert thinking tool request");
13796
13797        assert_eq!(
13798            internal.sampling_params.response_completion_boundary,
13799            ResponseCompletionBoundary::AfterDelimiterAndPayload {
13800                delimiter: THINK_END_TAG.to_string(),
13801                alternate_envelope: Some(ferrum_types::ResponseCompletionEnvelope {
13802                    open_token_text: "<tool_call>".to_string(),
13803                    close_token_text: "</tool_call>".to_string(),
13804                    max_envelopes: 32,
13805                }),
13806            }
13807        );
13808    }
13809
13810    #[test]
13811    fn strict_json_schema_response_format_uses_guided_sampling_mode() {
13812        let request = chat_request(json!({
13813            "response_format": {
13814                "type": "json_schema",
13815                "json_schema": {
13816                    "name": "answer",
13817                    "strict": true,
13818                    "schema": {
13819                        "type": "object",
13820                        "properties": {"answer": {"type": "string"}},
13821                        "required": ["answer"]
13822                    }
13823                }
13824            }
13825        }));
13826
13827        let internal = convert_chat_request(&request).expect("convert strict json_schema");
13828        assert!(
13829            internal
13830                .prompt
13831                .contains("response_format requires a single valid JSON value"),
13832            "response_format instruction should reach the model prompt: {}",
13833            internal.prompt
13834        );
13835        let ferrum_types::ResponseFormat::JsonSchema(schema) =
13836            internal.sampling_params.response_format
13837        else {
13838            panic!(
13839                "strict json_schema must reach guided decoding, got {:?}",
13840                internal.sampling_params.response_format
13841            );
13842        };
13843        let schema: serde_json::Value = serde_json::from_str(&schema).unwrap();
13844        assert_eq!(schema["type"], "object");
13845        assert_eq!(schema["properties"]["answer"]["type"], "string");
13846        assert_eq!(schema["required"], json!(["answer"]));
13847    }
13848
13849    #[tokio::test]
13850    async fn strict_json_schema_validates_non_streaming_response() {
13851        let request = chat_request(json!({
13852            "response_format": {
13853                "type": "json_schema",
13854                "json_schema": {
13855                    "name": "answer",
13856                    "strict": true,
13857                    "schema": {
13858                        "type": "object",
13859                        "properties": {"answer": {"type": "string"}},
13860                        "required": ["answer"]
13861                    }
13862                }
13863            }
13864        }));
13865        let response = chat_completions_handler(
13866            State(state_with_stub("{\"answer\":\"yes\"}")),
13867            HeaderMap::new(),
13868            Ok(Json(request)),
13869        )
13870        .await
13871        .expect("strict response");
13872        assert_eq!(response.status(), AxumStatusCode::OK);
13873        let body = response_json(response).await;
13874        assert_eq!(
13875            body["choices"][0]["message"]["content"],
13876            "{\"answer\":\"yes\"}"
13877        );
13878    }
13879
13880    #[tokio::test]
13881    async fn strict_json_schema_validates_non_streaming_response_after_reasoning_block() {
13882        let request = chat_request(json!({
13883            "response_format": {
13884                "type": "json_schema",
13885                "json_schema": {
13886                    "name": "answer",
13887                    "strict": true,
13888                    "schema": {
13889                        "type": "object",
13890                        "properties": {"answer": {"type": "string"}},
13891                        "required": ["answer"]
13892                    }
13893                }
13894            }
13895        }));
13896        let response = chat_completions_handler(
13897            State(state_with_stub(
13898                "<think>\nreasoning\n</think>\n\n{\"answer\":\"yes\"}",
13899            )),
13900            HeaderMap::new(),
13901            Ok(Json(request)),
13902        )
13903        .await
13904        .expect("strict response with reasoning");
13905        assert_eq!(response.status(), AxumStatusCode::OK);
13906        let body = response_json(response).await;
13907        assert_eq!(
13908            body["choices"][0]["message"]["content"],
13909            "{\"answer\":\"yes\"}"
13910        );
13911        assert_eq!(body["choices"][0]["message"]["reasoning"], "\nreasoning\n");
13912    }
13913
13914    #[tokio::test]
13915    async fn strict_json_schema_validates_streaming_final_response() {
13916        let response = post_json(
13917            router_with_stub("{\"answer\":\"yes\"}"),
13918            "/v1/chat/completions",
13919            json!({
13920                "model": "stub-model",
13921                "messages": [{"role": "user", "content": "Return an answer object."}],
13922                "stream": true,
13923                "response_format": {
13924                    "type": "json_schema",
13925                    "json_schema": {
13926                        "name": "answer",
13927                        "strict": true,
13928                        "schema": {
13929                            "type": "object",
13930                            "properties": {"answer": {"type": "string"}},
13931                            "required": ["answer"]
13932                        }
13933                    }
13934                }
13935            }),
13936        )
13937        .await;
13938        assert_eq!(response.status(), AxumStatusCode::OK);
13939        let body = response_text(response).await;
13940        assert!(body.contains("data: [DONE]"), "missing DONE: {body}");
13941        assert!(
13942            body.contains("\\\"answer\\\":\\\"yes\\\""),
13943            "strict streaming content missing: {body}"
13944        );
13945        assert!(
13946            !body.contains("\"error\""),
13947            "valid strict streaming response should not emit error: {body}"
13948        );
13949    }
13950
13951    #[tokio::test]
13952    async fn strict_json_schema_validates_streaming_final_response_after_reasoning_block() {
13953        let response = post_json(
13954            router_with_stub_stream_chunks(&[
13955                "<think>\nreasoning",
13956                "\n</think>\n\n",
13957                "{\"answer\":\"yes\"}",
13958            ]),
13959            "/v1/chat/completions",
13960            json!({
13961                "model": "stub-model",
13962                "messages": [{"role": "user", "content": "Return an answer object."}],
13963                "stream": true,
13964                "response_format": {
13965                    "type": "json_schema",
13966                    "json_schema": {
13967                        "name": "answer",
13968                        "strict": true,
13969                        "schema": {
13970                            "type": "object",
13971                            "properties": {"answer": {"type": "string"}},
13972                            "required": ["answer"]
13973                        }
13974                    }
13975                }
13976            }),
13977        )
13978        .await;
13979        assert_eq!(response.status(), AxumStatusCode::OK);
13980        let body = response_text(response).await;
13981        assert!(body.contains("data: [DONE]"), "missing DONE: {body}");
13982        assert!(
13983            body.contains("\\\"answer\\\":\\\"yes\\\""),
13984            "strict streaming content missing: {body}"
13985        );
13986        assert!(
13987            body.contains(r#""reasoning":"\nreasoning\n""#),
13988            "strict streaming should keep reasoning separate: {body}"
13989        );
13990        assert!(
13991            !body.contains("\"error\""),
13992            "valid strict streaming response should not emit error: {body}"
13993        );
13994    }
13995
13996    #[tokio::test]
13997    async fn strict_json_schema_invalid_streaming_output_emits_error_event() {
13998        let response = post_json(
13999            router_with_stub("not json"),
14000            "/v1/chat/completions",
14001            json!({
14002                "model": "stub-model",
14003                "messages": [{"role": "user", "content": "Return an answer object."}],
14004                "stream": true,
14005                "response_format": {
14006                    "type": "json_schema",
14007                    "json_schema": {
14008                        "name": "answer",
14009                        "strict": true,
14010                        "schema": {
14011                            "type": "object",
14012                            "properties": {"answer": {"type": "string"}},
14013                            "required": ["answer"]
14014                        }
14015                    }
14016                }
14017            }),
14018        )
14019        .await;
14020        assert_eq!(response.status(), AxumStatusCode::OK);
14021        let body = response_text(response).await;
14022        assert!(body.contains("data: [DONE]"), "missing DONE: {body}");
14023        assert!(
14024            body.contains("\"type\":\"internal_server_error\""),
14025            "strict streaming validation failure should emit OpenAI error: {body}"
14026        );
14027        assert!(
14028            body.contains("\"param\":\"response_format.json_schema\""),
14029            "strict streaming validation error should identify schema param: {body}"
14030        );
14031        assert!(
14032            body.contains("invalid JSON"),
14033            "strict streaming validation should report invalid JSON: {body}"
14034        );
14035        assert!(
14036            !body.contains("not json"),
14037            "strict streaming must not emit invalid partial deltas before validation failure: {body}"
14038        );
14039    }
14040
14041    #[tokio::test]
14042    async fn route_strict_json_schema_supported_schema_passes_100_runs() {
14043        let request_body = json!({
14044            "model": "stub-model",
14045            "messages": [{"role": "user", "content": "Return an answer object."}],
14046            "response_format": {
14047                "type": "json_schema",
14048                "json_schema": {
14049                    "name": "answer",
14050                    "strict": true,
14051                    "schema": {
14052                        "type": "object",
14053                        "properties": {"answer": {"type": "string"}},
14054                        "required": ["answer"]
14055                    }
14056                }
14057            }
14058        });
14059        let router = router_with_stub("{\"answer\":\"yes\"}");
14060        for run in 0..100 {
14061            let response =
14062                post_json(router.clone(), "/v1/chat/completions", request_body.clone()).await;
14063            assert_eq!(
14064                response.status(),
14065                AxumStatusCode::OK,
14066                "strict schema run {run} returned non-200"
14067            );
14068            let body = response_json(response).await;
14069            let content = body["choices"][0]["message"]["content"]
14070                .as_str()
14071                .unwrap_or("");
14072            assert_eq!(
14073                content, "{\"answer\":\"yes\"}",
14074                "strict schema run {run} returned unexpected content"
14075            );
14076            let parsed: serde_json::Value =
14077                serde_json::from_str(content).expect("strict content JSON");
14078            assert_eq!(parsed["answer"], "yes");
14079        }
14080    }
14081
14082    #[test]
14083    fn cache_metrics_use_engine_real_kv_snapshot_when_available() {
14084        let cache = CacheRuntimeState::default();
14085        let policy = CachePolicy {
14086            prefix_cache_enabled: true,
14087            session_cache_mode: "memory".to_string(),
14088            session_cache_max_entries: 128,
14089            session_cache_max_tokens: 4096,
14090        };
14091        cache.record_prefix_prompt("alpha beta gamma", &policy);
14092        cache.record_prefix_prompt("alpha beta delta", &policy);
14093
14094        let engine_snapshot = json!({
14095            "position": "real-kv-reuse",
14096            "source": "llama-family-paged-block-prefix-cache",
14097            "enabled": true,
14098            "hits": 7,
14099            "misses": 3,
14100            "evictions": 1,
14101            "saved_prefill_tokens": 64,
14102            "entries": 5,
14103            "bytes": 8192,
14104            "block_size": 16,
14105            "kv_dtype": "fp16",
14106            "selected_pipeline_mode": "batch",
14107            "selected_stage_bridge": "host",
14108            "stage_count": 2,
14109        });
14110
14111        let health = cache.health_json(&policy, Some(&engine_snapshot));
14112        let prefix = &health["prefix_cache"];
14113        assert_eq!(prefix["position"], "real-kv-reuse");
14114        assert_eq!(prefix["source"], "llama-family-paged-block-prefix-cache");
14115        assert_eq!(prefix["hits"], 7);
14116        assert_eq!(prefix["misses"], 3);
14117        assert_eq!(prefix["evictions"], 1);
14118        assert_eq!(prefix["saved_prefill_tokens"], 64);
14119        assert_eq!(prefix["entries"], 5);
14120        assert_eq!(prefix["bytes"], 8192);
14121        assert_eq!(prefix["block_size"], 16);
14122        assert_eq!(prefix["kv_dtype"], "fp16");
14123        assert_eq!(prefix["selected_pipeline_mode"], "batch");
14124        assert_eq!(prefix["selected_stage_bridge"], "host");
14125        assert_eq!(prefix["stage_count"], 2);
14126
14127        let metrics = cache.prometheus_metrics(Some(&engine_snapshot));
14128        assert!(metrics.contains("ferrum_prefix_cache_hits_total 7\n"));
14129        assert!(metrics.contains("ferrum_prefix_cache_misses_total 3\n"));
14130        assert!(metrics.contains("ferrum_prefix_cache_saved_prefill_tokens_total 64\n"));
14131        assert!(metrics.contains("ferrum_prefix_cache_entries 5\n"));
14132        assert!(metrics.contains("ferrum_prefix_cache_bytes 8192\n"));
14133    }
14134
14135    #[tokio::test]
14136    async fn strict_json_schema_invalid_model_output_fails_before_response() {
14137        let request = chat_request(json!({
14138            "response_format": {
14139                "type": "json_schema",
14140                "json_schema": {
14141                    "name": "answer",
14142                    "strict": true,
14143                    "schema": {
14144                        "type": "object",
14145                        "properties": {"answer": {"type": "string"}},
14146                        "required": ["answer"]
14147                    }
14148                }
14149            }
14150        }));
14151        let err = chat_completions_handler(
14152            State(state_with_stub("not json")),
14153            HeaderMap::new(),
14154            Ok(Json(request)),
14155        )
14156        .await
14157        .expect_err("invalid strict response should fail");
14158        let (status, body) = error_json(err).await;
14159        assert_eq!(status, AxumStatusCode::INTERNAL_SERVER_ERROR);
14160        assert_eq!(body["error"]["type"], "internal_server_error");
14161        assert!(body["error"]["message"]
14162            .as_str()
14163            .unwrap()
14164            .contains("json_schema.strict"));
14165    }
14166
14167    #[tokio::test]
14168    async fn strict_json_schema_does_not_rely_on_markdown_fence_stripping() {
14169        let request = chat_request(json!({
14170            "response_format": {
14171                "type": "json_schema",
14172                "json_schema": {
14173                    "name": "answer",
14174                    "strict": true,
14175                    "schema": {
14176                        "type": "object",
14177                        "properties": {"answer": {"type": "string"}},
14178                        "required": ["answer"]
14179                    }
14180                }
14181            }
14182        }));
14183        let err = chat_completions_handler(
14184            State(state_with_stub("```json\n{\"answer\":\"yes\"}\n```")),
14185            HeaderMap::new(),
14186            Ok(Json(request)),
14187        )
14188        .await
14189        .expect_err("strict schema should fail fenced JSON instead of repairing it");
14190        let (status, body) = error_json(err).await;
14191        assert_eq!(status, AxumStatusCode::INTERNAL_SERVER_ERROR);
14192        assert_eq!(body["error"]["type"], "internal_server_error");
14193        assert!(body["error"]["message"]
14194            .as_str()
14195            .unwrap()
14196            .contains("json_schema.strict: invalid JSON"));
14197    }
14198}